mirror of
https://github.com/luxfi/bridge.git
synced 2026-09-24 00:26:46 +00:00
feat(bridge): unified Go binary — embedded SPA + native networks/tokens/limits, proxied swap/quote/rate
Mirrors the luxfi/explorer pattern: cmd/bridge/main.go embeds the SPA build via go:embed and exposes /v1/bridge/* on a single port. Read paths (networks, tokens, exchanges, limits) are answered natively from a YAML config — no Postgres. MPC-heavy paths (quote, rate, swaps, explorer) reverse-proxy to the legacy Node backend until the native port lands. Image: cmd/bridge/Dockerfile produces ~25 MB distroless. Config: cmd/bridge/networks.example.yaml. Migration target: Hanzo Base (SQLite) + luxfi/mpc direct calls.
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
# Single-image Lux Bridge: SPA + Go API in one binary.
|
||||
#
|
||||
# Stage 1 (ui-build): produce static/ from the bridge UI (app/bridge).
|
||||
# Stage 2 (go-build): embed static/ via go:embed and produce the binary.
|
||||
# Stage 3 (final): distroless runtime, ~25 MB.
|
||||
#
|
||||
# Build (from repo root):
|
||||
# docker build -f cmd/bridge/Dockerfile -t ghcr.io/luxfi/bridge:dev .
|
||||
|
||||
# ── 1. UI build ───────────────────────────────────────────────────────────
|
||||
FROM node:20-alpine AS ui-build
|
||||
WORKDIR /ui
|
||||
RUN corepack enable && corepack prepare [email protected] --activate
|
||||
COPY app/bridge/package.json app/bridge/pnpm-lock.yaml* ./
|
||||
RUN pnpm install --frozen-lockfile || pnpm install
|
||||
COPY app/bridge/ ./
|
||||
RUN pnpm build && \
|
||||
if [ -d dist ]; then mv dist out; fi
|
||||
|
||||
# ── 2. Go build ───────────────────────────────────────────────────────────
|
||||
FROM golang:1.26-alpine AS go-build
|
||||
WORKDIR /src
|
||||
RUN apk add --no-cache git
|
||||
COPY go.mod go.sum* ./
|
||||
RUN go mod download
|
||||
COPY cmd/ cmd/
|
||||
# Replace placeholder static/ with the real UI build.
|
||||
RUN rm -rf cmd/bridge/static && mkdir -p cmd/bridge/static
|
||||
COPY --from=ui-build /ui/out/ cmd/bridge/static/
|
||||
ARG VERSION=dev
|
||||
RUN CGO_ENABLED=0 go build -trimpath \
|
||||
-ldflags "-s -w -X main.version=${VERSION}" \
|
||||
-o /out/bridge ./cmd/bridge
|
||||
|
||||
# ── 3. Runtime ────────────────────────────────────────────────────────────
|
||||
FROM gcr.io/distroless/static-debian12:nonroot
|
||||
COPY --from=go-build /out/bridge /usr/local/bin/bridge
|
||||
COPY cmd/bridge/networks.example.yaml /etc/bridge/networks.yaml
|
||||
USER nonroot:nonroot
|
||||
EXPOSE 8080
|
||||
ENTRYPOINT ["/usr/local/bin/bridge"]
|
||||
CMD ["--config=/etc/bridge/networks.yaml", "--addr=:8080"]
|
||||
@@ -0,0 +1,45 @@
|
||||
# lux-bridge
|
||||
|
||||
Single-image bridge: SPA + API in one Go binary.
|
||||
|
||||
## Why
|
||||
|
||||
The legacy stack (Node API + React UI + Postgres + Prisma) is many moving parts. This binary collapses the read-path (networks, tokens, exchanges, limits) into a Go service that needs zero database. Heavy paths (quote, rate, swaps, explorer) reverse-proxy to the existing Node backend until the native port lands; pointing them away is a one-flag change.
|
||||
|
||||
## Run
|
||||
|
||||
```
|
||||
go build -o bridge ./cmd/bridge
|
||||
./bridge --config cmd/bridge/networks.example.yaml --backend http://localhost:5000
|
||||
```
|
||||
|
||||
Routes:
|
||||
|
||||
| Path | Source |
|
||||
| --- | --- |
|
||||
| `/` | embedded SPA (or `--static <dir>` to override) |
|
||||
| `/envs.js` | runtime config (`window.ENV`) |
|
||||
| `/health` | service health |
|
||||
| `/v1/bridge/networks` | YAML config |
|
||||
| `/v1/bridge/tokens` | YAML config (filterable by `?network=`) |
|
||||
| `/v1/bridge/exchanges` | YAML config |
|
||||
| `/v1/bridge/limits` | YAML config |
|
||||
| `/v1/bridge/quote` | proxied → Node backend |
|
||||
| `/v1/bridge/rate` | proxied |
|
||||
| `/v1/bridge/swaps` | proxied |
|
||||
| `/v1/bridge/explorer/*` | proxied |
|
||||
| `/v1/bridge/settings` | proxied |
|
||||
|
||||
Without `--backend`, proxied routes return `503 backend_unavailable`.
|
||||
|
||||
## Build the image
|
||||
|
||||
```
|
||||
docker build -f cmd/bridge/Dockerfile -t ghcr.io/luxfi/bridge:dev .
|
||||
```
|
||||
|
||||
The Dockerfile builds the UI from `app/bridge/` and embeds it into the Go binary. Final image is distroless (~25 MB).
|
||||
|
||||
## Migration plan
|
||||
|
||||
The Node backend's `swaps`, `quote`, `rate`, `mpc-bridge`, `teleport-processor`, and `mpc-service` modules — plus the Prisma schema (`Network`, `Currency`, `Swap`, `Transaction`, `Quote`, `RpcNode`) — are next to port. Target storage is Hanzo Base (SQLite-embedded). MPC orchestration calls `github.com/luxfi/mpc` directly.
|
||||
@@ -0,0 +1,102 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// API serves the bridge HTTP surface. Read paths (networks, tokens,
|
||||
// exchanges, limits) are answered natively from Config — no Postgres
|
||||
// hit. MPC-heavy paths (quote, rate, swaps, explorer) reverse-proxy to
|
||||
// the legacy Node backend at backendURL until the native port lands.
|
||||
type API struct {
|
||||
cfg Config
|
||||
backend string
|
||||
proxy *httputil.ReverseProxy
|
||||
}
|
||||
|
||||
func NewAPI(cfg Config, backendURL string) *API {
|
||||
a := &API{cfg: cfg, backend: backendURL}
|
||||
if backendURL != "" {
|
||||
u, err := url.Parse(backendURL)
|
||||
if err == nil {
|
||||
a.proxy = httputil.NewSingleHostReverseProxy(u)
|
||||
a.proxy.Director = stripPathPrefix(u, "/v1/bridge")
|
||||
}
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
// Register mounts handlers on the given mux. The /v1/bridge prefix matches
|
||||
// what the SPA fetches and what hanzo/ingress routes externally.
|
||||
func (a *API) Register(mux *http.ServeMux) {
|
||||
mux.HandleFunc("/v1/bridge/networks", a.networks)
|
||||
mux.HandleFunc("/v1/bridge/tokens", a.tokens)
|
||||
mux.HandleFunc("/v1/bridge/exchanges", a.exchanges)
|
||||
mux.HandleFunc("/v1/bridge/limits", a.limits)
|
||||
|
||||
// Proxied (require Node backend). When backend is unset these 503
|
||||
// to make the missing dependency obvious instead of silently 404ing.
|
||||
mux.HandleFunc("/v1/bridge/quote", a.proxied)
|
||||
mux.HandleFunc("/v1/bridge/rate", a.proxied)
|
||||
mux.HandleFunc("/v1/bridge/settings", a.proxied)
|
||||
mux.HandleFunc("/v1/bridge/swaps", a.proxied)
|
||||
mux.HandleFunc("/v1/bridge/swaps/", a.proxied)
|
||||
mux.HandleFunc("/v1/bridge/explorer/", a.proxied)
|
||||
}
|
||||
|
||||
func (a *API) networks(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, a.cfg.Networks)
|
||||
}
|
||||
|
||||
func (a *API) tokens(w http.ResponseWriter, r *http.Request) {
|
||||
if net := r.URL.Query().Get("network"); net != "" {
|
||||
var out []Token
|
||||
for _, t := range a.cfg.Tokens {
|
||||
if t.Network == net {
|
||||
out = append(out, t)
|
||||
}
|
||||
}
|
||||
writeJSON(w, out)
|
||||
return
|
||||
}
|
||||
writeJSON(w, a.cfg.Tokens)
|
||||
}
|
||||
|
||||
func (a *API) exchanges(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, a.cfg.Exchanges)
|
||||
}
|
||||
|
||||
func (a *API) limits(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, a.cfg.Limits)
|
||||
}
|
||||
|
||||
func (a *API) proxied(w http.ResponseWriter, r *http.Request) {
|
||||
if a.proxy == nil {
|
||||
http.Error(w, `{"error":"backend_unavailable","detail":"set BRIDGE_BACKEND_URL to enable swap/quote/explorer routes"}`, http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
a.proxy.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func writeJSON(w http.ResponseWriter, v any) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(v)
|
||||
}
|
||||
|
||||
// stripPathPrefix returns a Director that rewrites /v1/bridge/<x> to /<x>
|
||||
// so requests reach the Node backend's existing route paths.
|
||||
func stripPathPrefix(target *url.URL, prefix string) func(*http.Request) {
|
||||
return func(r *http.Request) {
|
||||
r.URL.Scheme = target.Scheme
|
||||
r.URL.Host = target.Host
|
||||
r.Host = target.Host
|
||||
r.URL.Path = strings.TrimPrefix(r.URL.Path, prefix)
|
||||
if r.URL.Path == "" {
|
||||
r.URL.Path = "/"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// Config is the bridge runtime configuration. The shape mirrors the
|
||||
// data the Node backend serves from the `Network`/`Currency` Prisma
|
||||
// tables — keeping it in YAML lets us source-control supported chains
|
||||
// + tokens and read them natively without spinning up Postgres.
|
||||
type Config struct {
|
||||
Brand Brand `yaml:"brand"`
|
||||
Networks []Network `yaml:"networks"`
|
||||
Tokens []Token `yaml:"tokens"`
|
||||
Limits Limits `yaml:"limits"`
|
||||
Exchanges []Exchange `yaml:"exchanges"`
|
||||
}
|
||||
|
||||
// Brand controls the SPA's runtime config (window.ENV) and per-host assets.
|
||||
type Brand struct {
|
||||
Name string `yaml:"name"`
|
||||
Title string `yaml:"title"`
|
||||
LogoURL string `yaml:"logoUrl"`
|
||||
IconURL string `yaml:"iconUrl"`
|
||||
BrandHost string `yaml:"brandHost"`
|
||||
}
|
||||
|
||||
// Network is a supported chain. internalName is the slug used by the SPA;
|
||||
// chainID is the EVM chain ID (or substrate paraID); explorerTpl is the
|
||||
// {tx} / {addr} URL template the SPA renders.
|
||||
type Network struct {
|
||||
InternalName string `yaml:"internalName"`
|
||||
DisplayName string `yaml:"displayName"`
|
||||
NativeCurrency string `yaml:"nativeCurrency"`
|
||||
IsTestnet bool `yaml:"isTestnet"`
|
||||
IsFeatured bool `yaml:"isFeatured"`
|
||||
Logo string `yaml:"logo"`
|
||||
ChainID string `yaml:"chainId"`
|
||||
Type string `yaml:"type"` // evm | substrate | bitcoin
|
||||
AvgCompletion string `yaml:"avgCompletion"`
|
||||
TxExplorerTpl string `yaml:"txExplorerTpl"`
|
||||
AddrExplorerTpl string `yaml:"addrExplorerTpl"`
|
||||
}
|
||||
|
||||
// Token is an asset bridged on one or more networks. The `decimals` and
|
||||
// `contract` fields are per-network; expand these once we add multi-chain
|
||||
// token mapping (matches the Node backend's `Currency` model).
|
||||
type Token struct {
|
||||
Asset string `yaml:"asset"`
|
||||
Name string `yaml:"name"`
|
||||
Logo string `yaml:"logo"`
|
||||
Decimals int `yaml:"decimals"`
|
||||
Contract string `yaml:"contract"`
|
||||
Network string `yaml:"network"`
|
||||
}
|
||||
|
||||
// Limits are per-token min/max swap caps. Real impl reads from KMS/admin,
|
||||
// but for the SPA's read paths a static config is fine.
|
||||
type Limits struct {
|
||||
MinUSD float64 `yaml:"minUSD"`
|
||||
MaxUSD float64 `yaml:"maxUSD"`
|
||||
Per map[string]Caps `yaml:"per"`
|
||||
}
|
||||
|
||||
type Caps struct {
|
||||
MinUSD float64 `yaml:"minUSD"`
|
||||
MaxUSD float64 `yaml:"maxUSD"`
|
||||
}
|
||||
|
||||
// Exchange is a CEX integration listing.
|
||||
type Exchange struct {
|
||||
InternalName string `yaml:"internalName"`
|
||||
DisplayName string `yaml:"displayName"`
|
||||
Logo string `yaml:"logo"`
|
||||
URL string `yaml:"url"`
|
||||
}
|
||||
|
||||
func LoadConfig(path string) (Config, error) {
|
||||
if path == "" {
|
||||
return defaultConfig(), nil
|
||||
}
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return defaultConfig(), nil
|
||||
}
|
||||
return Config{}, fmt.Errorf("read %s: %w", path, err)
|
||||
}
|
||||
var c Config
|
||||
if err := yaml.Unmarshal(data, &c); err != nil {
|
||||
return Config{}, fmt.Errorf("parse %s: %w", path, err)
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func defaultConfig() Config {
|
||||
return Config{
|
||||
Brand: Brand{
|
||||
Name: "Lux Bridge",
|
||||
Title: "Lux Bridge",
|
||||
LogoURL: "/logo.svg",
|
||||
IconURL: "/icon.svg",
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// staticFS is the embedded SPA. The Dockerfile populates static/ from a
|
||||
// luxfi/bridge UI build before the Go build runs; for local dev a
|
||||
// placeholder index.html is committed so go:embed always succeeds.
|
||||
//
|
||||
//go:embed all:static
|
||||
var staticFS embed.FS
|
||||
|
||||
// Frontend serves the embedded SPA, runtime config (/envs.js), and per-host
|
||||
// brand assets (/icon.svg, /logo.svg). Brand assets read from disk on every
|
||||
// request so a deploy can swap them without rebuilding the binary.
|
||||
type Frontend struct {
|
||||
cfg Config
|
||||
root fs.FS
|
||||
index []byte
|
||||
overlay string // optional disk dir for SPA + brand override
|
||||
}
|
||||
|
||||
func NewFrontend(cfg Config, overlay string) (*Frontend, error) {
|
||||
root, err := fs.Sub(staticFS, "static")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("sub-fs: %w", err)
|
||||
}
|
||||
idx, err := fs.ReadFile(root, "index.html")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read index.html: %w", err)
|
||||
}
|
||||
return &Frontend{cfg: cfg, root: root, index: idx, overlay: overlay}, nil
|
||||
}
|
||||
|
||||
func (f *Frontend) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/envs.js":
|
||||
f.serveEnvs(w, r)
|
||||
return
|
||||
case "/icon.svg":
|
||||
f.serveBrandAsset(w, r, "icon.svg", f.cfg.Brand.IconURL)
|
||||
return
|
||||
case "/logo.svg":
|
||||
f.serveBrandAsset(w, r, "logo.svg", f.cfg.Brand.LogoURL)
|
||||
return
|
||||
}
|
||||
|
||||
// Try disk overlay first so devs can hot-swap the SPA without rebuild.
|
||||
if f.overlay != "" {
|
||||
p := filepath.Join(f.overlay, strings.TrimPrefix(r.URL.Path, "/"))
|
||||
if data, err := os.ReadFile(p); err == nil {
|
||||
http.ServeContent(w, r, p, fileModTime(p), bytes.NewReader(data))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Embedded asset?
|
||||
cleaned := strings.TrimPrefix(r.URL.Path, "/")
|
||||
if cleaned != "" {
|
||||
if data, err := fs.ReadFile(f.root, cleaned); err == nil {
|
||||
http.ServeContent(w, r, cleaned, time.Time{}, bytes.NewReader(data))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// SPA fallback — let React Router handle the route.
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
_, _ = w.Write(f.index)
|
||||
}
|
||||
|
||||
func (f *Frontend) serveEnvs(w http.ResponseWriter, r *http.Request) {
|
||||
envs := map[string]any{
|
||||
"brand": f.cfg.Brand,
|
||||
"apiBase": "/v1/bridge",
|
||||
}
|
||||
body, _ := json.Marshal(envs)
|
||||
w.Header().Set("Content-Type", "application/javascript")
|
||||
fmt.Fprintf(w, "window.ENV = %s;", body)
|
||||
}
|
||||
|
||||
func (f *Frontend) serveBrandAsset(w http.ResponseWriter, r *http.Request, name, fallback string) {
|
||||
if f.overlay != "" {
|
||||
if data, err := os.ReadFile(filepath.Join(f.overlay, name)); err == nil {
|
||||
w.Header().Set("Content-Type", "image/svg+xml")
|
||||
_, _ = w.Write(data)
|
||||
return
|
||||
}
|
||||
}
|
||||
if fallback != "" {
|
||||
http.Redirect(w, r, fallback, http.StatusFound)
|
||||
return
|
||||
}
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
|
||||
func fileModTime(path string) time.Time {
|
||||
if path == "" {
|
||||
return time.Time{}
|
||||
}
|
||||
fi, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return time.Time{}
|
||||
}
|
||||
return fi.ModTime()
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
// Package main is the unified Lux bridge: a single Go binary that embeds
|
||||
// the SPA, serves the bridge API natively for read paths (networks, tokens,
|
||||
// quotes), and proxies MPC-heavy paths (swap orchestration, signer state)
|
||||
// to the legacy Node backend during the migration to a fully native impl.
|
||||
//
|
||||
// Build: go build -o bridge ./cmd/bridge
|
||||
// Run: bridge --config /etc/bridge/networks.yaml
|
||||
//
|
||||
// Routes:
|
||||
//
|
||||
// / SPA (embedded; SPA-routing fallback)
|
||||
// /envs.js runtime config window.ENV = {...}
|
||||
// /icon.svg, /logo.svg per-host brand assets (disk override)
|
||||
// /health service health
|
||||
// /v1/bridge/networks supported chains
|
||||
// /v1/bridge/tokens tokens per chain
|
||||
// /v1/bridge/quote price quote (proxied)
|
||||
// /v1/bridge/rate exchange rate (proxied)
|
||||
// /v1/bridge/limits swap limits (proxied)
|
||||
// /v1/bridge/swaps/* swap CRUD (proxied)
|
||||
// /v1/bridge/explorer/* tx lookup (proxied)
|
||||
// /v1/bridge/settings config (proxied)
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
var version = "dev"
|
||||
|
||||
func main() {
|
||||
cfgPath := flag.String("config", envOr("BRIDGE_CONFIG", "/etc/bridge/networks.yaml"), "path to networks.yaml")
|
||||
addr := flag.String("addr", envOr("BRIDGE_ADDR", ":8080"), "listen address")
|
||||
backend := flag.String("backend", envOr("BRIDGE_BACKEND_URL", ""), "legacy Node backend URL for proxied routes (empty disables proxy)")
|
||||
staticDir := flag.String("static", envOr("BRIDGE_STATIC_DIR", ""), "override embedded SPA from disk")
|
||||
showVersion := flag.Bool("version", false, "print version and exit")
|
||||
flag.Parse()
|
||||
|
||||
if *showVersion {
|
||||
fmt.Println(version)
|
||||
return
|
||||
}
|
||||
|
||||
cfg, err := LoadConfig(*cfgPath)
|
||||
if err != nil {
|
||||
log.Fatalf("load config: %v", err)
|
||||
}
|
||||
|
||||
frontend, err := NewFrontend(cfg, *staticDir)
|
||||
if err != nil {
|
||||
log.Fatalf("frontend: %v", err)
|
||||
}
|
||||
|
||||
api := NewAPI(cfg, *backend)
|
||||
|
||||
mux := http.NewServeMux()
|
||||
api.Register(mux)
|
||||
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
fmt.Fprintf(w, `{"status":"ok","version":%q,"backend_proxy":%t}`, version, *backend != "")
|
||||
})
|
||||
mux.Handle("/", frontend)
|
||||
|
||||
srv := &http.Server{
|
||||
Addr: *addr,
|
||||
Handler: mux,
|
||||
ReadHeaderTimeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
go func() {
|
||||
log.Printf("lux-bridge %s listening on %s (backend=%q networks=%d)", version, *addr, *backend, len(cfg.Networks))
|
||||
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
log.Fatalf("listen: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
stop := make(chan os.Signal, 1)
|
||||
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
|
||||
<-stop
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
_ = srv.Shutdown(ctx)
|
||||
log.Println("shutdown complete")
|
||||
}
|
||||
|
||||
func envOr(k, fallback string) string {
|
||||
if v := os.Getenv(k); v != "" {
|
||||
return v
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
# Example bridge config. Mount at /etc/bridge/networks.yaml.
|
||||
# Adding networks here is enough — no DB migration, no code change.
|
||||
# Schema: github.com/luxfi/bridge/cmd/bridge/config.go::Config
|
||||
brand:
|
||||
name: Lux Bridge
|
||||
title: Lux Bridge
|
||||
logoUrl: /logo.svg
|
||||
iconUrl: /icon.svg
|
||||
brandHost: bridge.lux.network
|
||||
|
||||
networks:
|
||||
- internalName: lux-mainnet
|
||||
displayName: Lux
|
||||
nativeCurrency: LUX
|
||||
chainId: "96369"
|
||||
type: evm
|
||||
isFeatured: true
|
||||
avgCompletion: 1m
|
||||
txExplorerTpl: https://explore.lux.network/tx/{tx}
|
||||
addrExplorerTpl: https://explore.lux.network/address/{addr}
|
||||
|
||||
- internalName: ethereum-mainnet
|
||||
displayName: Ethereum
|
||||
nativeCurrency: ETH
|
||||
chainId: "1"
|
||||
type: evm
|
||||
avgCompletion: 5m
|
||||
txExplorerTpl: https://etherscan.io/tx/{tx}
|
||||
addrExplorerTpl: https://etherscan.io/address/{addr}
|
||||
|
||||
- internalName: bsc-mainnet
|
||||
displayName: BNB Chain
|
||||
nativeCurrency: BNB
|
||||
chainId: "56"
|
||||
type: evm
|
||||
avgCompletion: 2m
|
||||
txExplorerTpl: https://bscscan.com/tx/{tx}
|
||||
addrExplorerTpl: https://bscscan.com/address/{addr}
|
||||
|
||||
tokens:
|
||||
- asset: LUX
|
||||
name: Lux
|
||||
decimals: 18
|
||||
network: lux-mainnet
|
||||
|
||||
- asset: USDT
|
||||
name: Tether USD
|
||||
decimals: 6
|
||||
contract: "0xdAC17F958D2ee523a2206206994597C13D831ec7"
|
||||
network: ethereum-mainnet
|
||||
|
||||
- asset: USDC
|
||||
name: USD Coin
|
||||
decimals: 6
|
||||
contract: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
|
||||
network: ethereum-mainnet
|
||||
|
||||
limits:
|
||||
minUSD: 10
|
||||
maxUSD: 100000
|
||||
per:
|
||||
USDT:
|
||||
minUSD: 10
|
||||
maxUSD: 100000
|
||||
USDC:
|
||||
minUSD: 10
|
||||
maxUSD: 100000
|
||||
@@ -0,0 +1,11 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Lux Bridge</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="root">Lux Bridge — placeholder. The Dockerfile populates static/ from a luxfi/bridge UI build before the Go build runs.</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,5 @@
|
||||
module github.com/luxfi/bridge
|
||||
|
||||
go 1.26.1
|
||||
|
||||
require gopkg.in/yaml.v3 v3.0.1
|
||||
@@ -0,0 +1,4 @@
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
Reference in New Issue
Block a user