mirror of
https://github.com/luxfi/bridge.git
synced 2026-09-24 00:26:46 +00:00
feat: universal Teleport bridge with 35+ chain plugins
- opnet-watcher: universal binary with plugin architecture - 3 functional plugins (OP_NET, Solana, TON) - Generic EVMPlugin handles all EVM chains (Ethereum, Base, Arbitrum, etc.) - 17 non-EVM chain stubs (Sui, NEAR, Cosmos, XRPL, ICP, etc.) - Threshold signing via luxfi/crypto (CGGMP21/FROST) - Separate txSigner hot wallet for on-chain submission - Multi-tenant API with hostname-based routing - Confirmation depth, backing sanity checks, EIP-155 local signing - manifests: 32 chain configs + 5 tenant configs (lux, zoo, hanzo, pars, spc) - k8s: fix chain IDs 1330663759→4294967299, ClusterIP services, KMS secrets - CI: conformance tests gate production deployment
This commit is contained in:
@@ -9,6 +9,7 @@ on:
|
||||
- 'app/bridge/**'
|
||||
- 'pkg/**'
|
||||
- 'contracts/**'
|
||||
- 'opnet-watcher/**'
|
||||
- '.github/workflows/build-and-push.yml'
|
||||
- 'Dockerfile*'
|
||||
- '.npmrc'
|
||||
@@ -122,10 +123,79 @@ jobs:
|
||||
cache-to: type=registry,ref=${{ env.IMAGE_PREFIX }}/bridge-server:buildcache,mode=max
|
||||
provenance: false
|
||||
|
||||
build-opnet-watcher:
|
||||
name: Build OPNET Watcher
|
||||
runs-on: lux-build-linux-amd64
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to GHCR
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.IMAGE_PREFIX }}/opnet-watcher
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=ref,event=pr
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=raw,value=latest,enable={{is_default_branch}}
|
||||
|
||||
- name: Build and push OPNET Watcher
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: ./opnet-watcher
|
||||
file: ./opnet-watcher/Dockerfile
|
||||
platforms: linux/amd64
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=registry,ref=${{ env.IMAGE_PREFIX }}/opnet-watcher:buildcache
|
||||
cache-to: type=registry,ref=${{ env.IMAGE_PREFIX }}/opnet-watcher:buildcache,mode=max
|
||||
provenance: false
|
||||
|
||||
contract-conformance:
|
||||
name: Teleport contract conformance
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
repository: luxfi/standard
|
||||
path: standard
|
||||
submodules: recursive
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Install Foundry
|
||||
uses: foundry-rs/foundry-toolchain@v1
|
||||
with:
|
||||
version: stable
|
||||
|
||||
- name: Teleport conformance tests
|
||||
working-directory: standard
|
||||
run: forge test --match-contract TeleportConformance -vvv
|
||||
|
||||
- name: OPNET conformance tests
|
||||
working-directory: standard
|
||||
run: forge test --match-contract OPNETConformance -vvv
|
||||
|
||||
deploy:
|
||||
name: Deploy to lux-k8s
|
||||
runs-on: lux-build-linux-amd64
|
||||
needs: [build-ui, build-server]
|
||||
needs: [build-ui, build-server, build-opnet-watcher, contract-conformance]
|
||||
if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -176,5 +246,7 @@ jobs:
|
||||
run: |
|
||||
kubectl rollout restart deployment/bridge-ui -n lux-bridge
|
||||
kubectl rollout restart deployment/bridge-server -n lux-bridge
|
||||
kubectl rollout restart deployment/opnet-watcher -n lux-bridge
|
||||
kubectl rollout status deployment/bridge-ui -n lux-bridge --timeout=300s
|
||||
kubectl rollout status deployment/bridge-server -n lux-bridge --timeout=300s
|
||||
kubectl rollout status deployment/opnet-watcher -n lux-bridge --timeout=300s
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ services:
|
||||
container_name: bridge-lux-id-db
|
||||
environment:
|
||||
- POSTGRES_USER=casdoor
|
||||
- POSTGRES_PASSWORD=casdoor123
|
||||
- POSTGRES_PASSWORD=${CASDOOR_DB_PASSWORD:-changeme}
|
||||
- POSTGRES_DB=casdoor
|
||||
volumes:
|
||||
- lux-id-db-data:/var/lib/postgresql/data
|
||||
|
||||
+12
-101
@@ -27,7 +27,7 @@ services:
|
||||
container_name: bridge-lux-id-db
|
||||
environment:
|
||||
- POSTGRES_USER=casdoor
|
||||
- POSTGRES_PASSWORD=casdoor123
|
||||
- POSTGRES_PASSWORD=${CASDOOR_DB_PASSWORD:-changeme}
|
||||
- POSTGRES_DB=casdoor
|
||||
volumes:
|
||||
- lux-id-db-data:/var/lib/postgresql/data
|
||||
@@ -41,76 +41,18 @@ services:
|
||||
timeout: 3s
|
||||
retries: 5
|
||||
|
||||
# KMS Service (Mock for local development)
|
||||
kms:
|
||||
image: hashicorp/vault:1.15
|
||||
container_name: bridge-kms
|
||||
ports:
|
||||
- "8200:8200"
|
||||
environment:
|
||||
- VAULT_DEV_ROOT_TOKEN_ID=myroot
|
||||
- VAULT_DEV_LISTEN_ADDRESS=0.0.0.0:8200
|
||||
cap_add:
|
||||
- IPC_LOCK
|
||||
networks:
|
||||
- bridge-network
|
||||
healthcheck:
|
||||
test: ["CMD", "vault", "status"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 5
|
||||
# KMS: use kms.hanzo.ai in production. For local dev, set KMS_TOKEN env var.
|
||||
# NATS/Consul removed — MPC uses ZAP p2p consensus.
|
||||
|
||||
# Infrastructure Services
|
||||
nats:
|
||||
image: nats:2.10-alpine
|
||||
container_name: bridge-nats
|
||||
ports:
|
||||
- "4223:4222"
|
||||
- "8223:8222"
|
||||
command: "-js -m 8222 --store_dir /data"
|
||||
volumes:
|
||||
- nats-data:/data
|
||||
networks:
|
||||
- bridge-network
|
||||
healthcheck:
|
||||
test: ["CMD", "nc", "-z", "localhost", "4222"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 5
|
||||
|
||||
consul:
|
||||
image: hashicorp/consul:1.16
|
||||
container_name: bridge-consul
|
||||
ports:
|
||||
- "8501:8500"
|
||||
- "8602:8600/udp"
|
||||
command: "agent -server -bootstrap-expect=1 -ui -client=0.0.0.0"
|
||||
volumes:
|
||||
- consul-data:/consul/data
|
||||
networks:
|
||||
- bridge-network
|
||||
environment:
|
||||
- CONSUL_BIND_INTERFACE=eth0
|
||||
healthcheck:
|
||||
test: ["CMD", "consul", "members"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 5
|
||||
|
||||
# MPC Nodes (3 nodes for 2-of-3 threshold)
|
||||
# MPC Nodes (3 nodes for 2-of-3 threshold) — ZAP p2p, no NATS/Consul
|
||||
mpc-node-0:
|
||||
image: ghcr.io/luxfi/mpc:latest
|
||||
container_name: bridge-mpc-0
|
||||
environment:
|
||||
- NODE_ID=mpc-node-0
|
||||
- NODE_PORT=6000
|
||||
- NATS_URL=nats://nats:4222
|
||||
- CONSUL_URL=http://consul:8500
|
||||
- DATA_DIR=/data/mpc
|
||||
- LOG_LEVEL=info
|
||||
- KMS_URL=http://kms:8200
|
||||
- KMS_TOKEN=myroot
|
||||
- KMS_PATH=secret/mpc
|
||||
volumes:
|
||||
- mpc-data-0:/data/mpc
|
||||
ports:
|
||||
@@ -118,26 +60,17 @@ services:
|
||||
networks:
|
||||
- bridge-network
|
||||
depends_on:
|
||||
nats:
|
||||
condition: service_healthy
|
||||
consul:
|
||||
condition: service_healthy
|
||||
kms:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
|
||||
mpc-node-1:
|
||||
image: ghcr.io/luxfi/lux-mpc:latest
|
||||
image: ghcr.io/luxfi/mpc:latest
|
||||
container_name: bridge-mpc-1
|
||||
environment:
|
||||
- NODE_ID=mpc-node-1
|
||||
- NODE_PORT=6000
|
||||
- NATS_URL=nats://nats:4222
|
||||
- CONSUL_URL=http://consul:8500
|
||||
- DATA_DIR=/data/mpc
|
||||
- LOG_LEVEL=info
|
||||
- KMS_URL=http://kms:8200
|
||||
- KMS_TOKEN=myroot
|
||||
- KMS_PATH=secret/mpc
|
||||
volumes:
|
||||
- mpc-data-1:/data/mpc
|
||||
ports:
|
||||
@@ -145,26 +78,17 @@ services:
|
||||
networks:
|
||||
- bridge-network
|
||||
depends_on:
|
||||
nats:
|
||||
condition: service_healthy
|
||||
consul:
|
||||
condition: service_healthy
|
||||
kms:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
|
||||
mpc-node-2:
|
||||
image: ghcr.io/luxfi/lux-mpc:latest
|
||||
image: ghcr.io/luxfi/mpc:latest
|
||||
container_name: bridge-mpc-2
|
||||
environment:
|
||||
- NODE_ID=mpc-node-2
|
||||
- NODE_PORT=6000
|
||||
- NATS_URL=nats://nats:4222
|
||||
- CONSUL_URL=http://consul:8500
|
||||
- DATA_DIR=/data/mpc
|
||||
- LOG_LEVEL=info
|
||||
- KMS_URL=http://kms:8200
|
||||
- KMS_TOKEN=myroot
|
||||
- KMS_PATH=secret/mpc
|
||||
volumes:
|
||||
- mpc-data-2:/data/mpc
|
||||
ports:
|
||||
@@ -172,11 +96,7 @@ services:
|
||||
networks:
|
||||
- bridge-network
|
||||
depends_on:
|
||||
nats:
|
||||
condition: service_healthy
|
||||
consul:
|
||||
condition: service_healthy
|
||||
kms:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
|
||||
# Database
|
||||
@@ -185,7 +105,7 @@ services:
|
||||
container_name: bridge-postgres
|
||||
environment:
|
||||
- POSTGRES_USER=bridge
|
||||
- POSTGRES_PASSWORD=bridge
|
||||
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-changeme}
|
||||
- POSTGRES_DB=bridge
|
||||
volumes:
|
||||
- postgres-data:/var/lib/postgresql/data
|
||||
@@ -207,20 +127,16 @@ services:
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
- PORT=5000
|
||||
- DATABASE_URL=postgresql://bridge:bridge@postgres:5432/bridge
|
||||
- NATS_URL=nats://nats:4222
|
||||
- CONSUL_URL=http://consul:8500
|
||||
- DATABASE_URL=postgresql://bridge:${POSTGRES_PASSWORD:-changeme}@postgres:5432/bridge
|
||||
- NODE_ID=bridge-server-0
|
||||
- MPC_THRESHOLD=2
|
||||
- MPC_TOTAL_NODES=3
|
||||
- MPC_KEY_PREFIX=bridge/mpc
|
||||
- KMS_URL=http://kms:8200
|
||||
- KMS_TOKEN=myroot
|
||||
# Lux ID (Casdoor) Authentication
|
||||
- AUTH_PROVIDER=casdoor
|
||||
- CASDOOR_ENDPOINT=http://lux-id:8000
|
||||
- CASDOOR_CLIENT_ID=lux-bridge
|
||||
- CASDOOR_CLIENT_SECRET=lux-bridge-secret
|
||||
- CASDOOR_CLIENT_SECRET=${CASDOOR_CLIENT_SECRET:-changeme}
|
||||
- CASDOOR_ORGANIZATION=lux-network
|
||||
- CASDOOR_APPLICATION=bridge
|
||||
ports:
|
||||
@@ -229,9 +145,6 @@ services:
|
||||
- bridge-network
|
||||
depends_on:
|
||||
- postgres
|
||||
- nats
|
||||
- consul
|
||||
- kms
|
||||
- lux-id
|
||||
- mpc-node-0
|
||||
- mpc-node-1
|
||||
@@ -271,8 +184,6 @@ services:
|
||||
volumes:
|
||||
lux-id-data:
|
||||
lux-id-db-data:
|
||||
nats-data:
|
||||
consul-data:
|
||||
mpc-data-0:
|
||||
mpc-data-1:
|
||||
mpc-data-2:
|
||||
|
||||
@@ -1,250 +0,0 @@
|
||||
services:
|
||||
# Infrastructure Services
|
||||
nats:
|
||||
image: nats:2.10-alpine
|
||||
container_name: bridge-nats
|
||||
ports:
|
||||
- "4222:4222"
|
||||
- "8222:8222"
|
||||
command: "-js -m 8222 --store_dir /data"
|
||||
volumes:
|
||||
- nats-data:/data
|
||||
networks:
|
||||
- bridge-network
|
||||
healthcheck:
|
||||
test: ["CMD", "nc", "-z", "localhost", "4222"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 5
|
||||
|
||||
consul:
|
||||
image: hashicorp/consul:1.17
|
||||
container_name: bridge-consul
|
||||
ports:
|
||||
- "8500:8500"
|
||||
- "8600:8600/udp"
|
||||
command: "agent -server -bootstrap-expect=1 -ui -client=0.0.0.0"
|
||||
volumes:
|
||||
- consul-data:/consul/data
|
||||
networks:
|
||||
- bridge-network
|
||||
environment:
|
||||
- CONSUL_BIND_INTERFACE=eth0
|
||||
healthcheck:
|
||||
test: ["CMD", "consul", "members"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 5
|
||||
|
||||
# Database
|
||||
postgres:
|
||||
image: postgres:15-alpine
|
||||
container_name: bridge-postgres
|
||||
environment:
|
||||
- POSTGRES_USER=bridge
|
||||
- POSTGRES_PASSWORD=bridge
|
||||
- POSTGRES_DB=bridge
|
||||
volumes:
|
||||
- postgres-data:/var/lib/postgresql/data
|
||||
- ./scripts/init.sql:/docker-entrypoint-initdb.d/init.sql
|
||||
ports:
|
||||
- "5432:5432"
|
||||
networks:
|
||||
- bridge-network
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U bridge"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 5
|
||||
|
||||
# MPC Nodes (3 nodes for 2-of-3 threshold)
|
||||
mpc-node-0:
|
||||
image: ghcr.io/luxfi/bridge-mpc:latest
|
||||
container_name: bridge-mpc-0
|
||||
environment:
|
||||
- NODE_ID=mpc-node-0
|
||||
- NODE_PORT=6000
|
||||
- NATS_URL=nats://nats:4222
|
||||
- CONSUL_URL=http://consul:8500
|
||||
- DATA_DIR=/data/mpc
|
||||
- LOG_LEVEL=info
|
||||
- MPC_THRESHOLD=2
|
||||
- MPC_TOTAL_NODES=3
|
||||
- MPC_KEY_PREFIX=bridge/mpc
|
||||
volumes:
|
||||
- mpc-data-0:/data/mpc
|
||||
ports:
|
||||
- "6000:6000"
|
||||
- "9090:9090"
|
||||
networks:
|
||||
- bridge-network
|
||||
depends_on:
|
||||
nats:
|
||||
condition: service_healthy
|
||||
consul:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:6000/health"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
||||
mpc-node-1:
|
||||
image: ghcr.io/luxfi/bridge-mpc:latest
|
||||
container_name: bridge-mpc-1
|
||||
environment:
|
||||
- NODE_ID=mpc-node-1
|
||||
- NODE_PORT=6000
|
||||
- NATS_URL=nats://nats:4222
|
||||
- CONSUL_URL=http://consul:8500
|
||||
- DATA_DIR=/data/mpc
|
||||
- LOG_LEVEL=info
|
||||
- MPC_THRESHOLD=2
|
||||
- MPC_TOTAL_NODES=3
|
||||
- MPC_KEY_PREFIX=bridge/mpc
|
||||
volumes:
|
||||
- mpc-data-1:/data/mpc
|
||||
ports:
|
||||
- "6001:6000"
|
||||
- "9091:9090"
|
||||
networks:
|
||||
- bridge-network
|
||||
depends_on:
|
||||
nats:
|
||||
condition: service_healthy
|
||||
consul:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:6000/health"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
||||
mpc-node-2:
|
||||
image: ghcr.io/luxfi/bridge-mpc:latest
|
||||
container_name: bridge-mpc-2
|
||||
environment:
|
||||
- NODE_ID=mpc-node-2
|
||||
- NODE_PORT=6000
|
||||
- NATS_URL=nats://nats:4222
|
||||
- CONSUL_URL=http://consul:8500
|
||||
- DATA_DIR=/data/mpc
|
||||
- LOG_LEVEL=info
|
||||
- MPC_THRESHOLD=2
|
||||
- MPC_TOTAL_NODES=3
|
||||
- MPC_KEY_PREFIX=bridge/mpc
|
||||
volumes:
|
||||
- mpc-data-2:/data/mpc
|
||||
ports:
|
||||
- "6002:6000"
|
||||
- "9092:9090"
|
||||
networks:
|
||||
- bridge-network
|
||||
depends_on:
|
||||
nats:
|
||||
condition: service_healthy
|
||||
consul:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:6000/health"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
||||
# Bridge Server
|
||||
bridge-server:
|
||||
image: ghcr.io/luxfi/bridge-server:latest
|
||||
container_name: bridge-server
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- PORT=3000
|
||||
- DATABASE_URL=postgresql://bridge:bridge@postgres:5432/bridge
|
||||
- NATS_URL=nats://nats:4222
|
||||
- CONSUL_URL=http://consul:8500
|
||||
- NODE_ID=bridge-server-0
|
||||
- MPC_THRESHOLD=2
|
||||
- MPC_TOTAL_NODES=3
|
||||
- MPC_KEY_PREFIX=bridge/mpc
|
||||
ports:
|
||||
- "3000:3000"
|
||||
networks:
|
||||
- bridge-network
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
nats:
|
||||
condition: service_healthy
|
||||
consul:
|
||||
condition: service_healthy
|
||||
mpc-node-0:
|
||||
condition: service_healthy
|
||||
mpc-node-1:
|
||||
condition: service_healthy
|
||||
mpc-node-2:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
||||
# Bridge UI
|
||||
bridge-ui:
|
||||
image: ghcr.io/luxfi/bridge-ui:latest
|
||||
container_name: bridge-ui
|
||||
environment:
|
||||
- NEXT_PUBLIC_API_URL=http://localhost:3000
|
||||
- NEXT_PUBLIC_CHAIN_ENV=mainnet
|
||||
ports:
|
||||
- "3001:3000"
|
||||
networks:
|
||||
- bridge-network
|
||||
depends_on:
|
||||
- bridge-server
|
||||
|
||||
# Monitoring (optional)
|
||||
prometheus:
|
||||
image: prom/prometheus:latest
|
||||
container_name: bridge-prometheus
|
||||
volumes:
|
||||
- ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml
|
||||
- prometheus-data:/prometheus
|
||||
ports:
|
||||
- "9093:9090"
|
||||
networks:
|
||||
- bridge-network
|
||||
command:
|
||||
- '--config.file=/etc/prometheus/prometheus.yml'
|
||||
- '--storage.tsdb.path=/prometheus'
|
||||
|
||||
grafana:
|
||||
image: grafana/grafana:latest
|
||||
container_name: bridge-grafana
|
||||
ports:
|
||||
- "3002:3000"
|
||||
networks:
|
||||
- bridge-network
|
||||
environment:
|
||||
- GF_SECURITY_ADMIN_PASSWORD=admin
|
||||
- GF_USERS_ALLOW_SIGN_UP=false
|
||||
volumes:
|
||||
- grafana-data:/var/lib/grafana
|
||||
- ./monitoring/grafana/dashboards:/etc/grafana/provisioning/dashboards
|
||||
- ./monitoring/grafana/datasources:/etc/grafana/provisioning/datasources
|
||||
|
||||
volumes:
|
||||
nats-data:
|
||||
consul-data:
|
||||
mpc-data-0:
|
||||
mpc-data-1:
|
||||
mpc-data-2:
|
||||
postgres-data:
|
||||
prometheus-data:
|
||||
grafana-data:
|
||||
|
||||
networks:
|
||||
bridge-network:
|
||||
driver: bridge
|
||||
ipam:
|
||||
config:
|
||||
- subnet: 172.20.0.0/16
|
||||
+364
-18
@@ -66,10 +66,7 @@ metadata:
|
||||
namespace: lux-bridge
|
||||
data:
|
||||
config.yaml: |
|
||||
nats:
|
||||
url: nats://nats:4222
|
||||
consul:
|
||||
address: consul:8500
|
||||
# MPC nodes coordinate via ZAP p2p — no NATS or Consul needed.
|
||||
environment: mainnet
|
||||
mpc_threshold: 3
|
||||
mpc_total_nodes: 5
|
||||
@@ -292,10 +289,6 @@ spec:
|
||||
secretKeyRef:
|
||||
name: bridge-secrets
|
||||
key: database-url
|
||||
- name: NATS_URL
|
||||
value: nats://nats:4222
|
||||
- name: CONSUL_URL
|
||||
value: http://consul:8500
|
||||
- name: NODE_ID
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
@@ -355,7 +348,7 @@ spec:
|
||||
- port: 3000
|
||||
targetPort: 3000
|
||||
name: http
|
||||
type: LoadBalancer
|
||||
type: ClusterIP
|
||||
---
|
||||
# Bridge UI Deployment
|
||||
apiVersion: apps/v1
|
||||
@@ -410,7 +403,7 @@ spec:
|
||||
- port: 80
|
||||
targetPort: 3000
|
||||
name: http
|
||||
type: LoadBalancer
|
||||
type: ClusterIP
|
||||
---
|
||||
# PostgreSQL StatefulSet
|
||||
apiVersion: apps/v1
|
||||
@@ -513,19 +506,29 @@ spec:
|
||||
- bridge-api.lux.network
|
||||
secretName: bridge-lux-tls
|
||||
- hosts:
|
||||
- bridge.pars.network
|
||||
secretName: bridge-pars-tls
|
||||
- bridge.zoo.ngo
|
||||
secretName: bridge-zoo-tls
|
||||
- hosts:
|
||||
- bridge.hanzo.ai
|
||||
secretName: bridge-hanzo-tls
|
||||
- hosts:
|
||||
- bridge.zoo.ngo
|
||||
secretName: bridge-zoo-tls
|
||||
- bridge.pars.network
|
||||
secretName: bridge-pars-tls
|
||||
- hosts:
|
||||
- bridge.spc.network
|
||||
secretName: bridge-spc-tls
|
||||
rules:
|
||||
# Lux Network (primary)
|
||||
- host: bridge.lux.network
|
||||
http:
|
||||
paths:
|
||||
- path: /v1/
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: opnet-watcher
|
||||
port:
|
||||
number: 8090
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
@@ -543,10 +546,17 @@ spec:
|
||||
name: bridge-server
|
||||
port:
|
||||
number: 3000
|
||||
# Pars Network — white-label, same bridge-ui backend
|
||||
- host: bridge.pars.network
|
||||
# Zoo — white-label
|
||||
- host: bridge.zoo.ngo
|
||||
http:
|
||||
paths:
|
||||
- path: /v1/
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: opnet-watcher
|
||||
port:
|
||||
number: 8090
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
@@ -558,6 +568,13 @@ spec:
|
||||
- host: bridge.hanzo.ai
|
||||
http:
|
||||
paths:
|
||||
- path: /v1/
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: opnet-watcher
|
||||
port:
|
||||
number: 8090
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
@@ -565,10 +582,17 @@ spec:
|
||||
name: bridge-ui
|
||||
port:
|
||||
number: 80
|
||||
# Zoo — white-label
|
||||
- host: bridge.zoo.ngo
|
||||
# Pars — white-label
|
||||
- host: bridge.pars.network
|
||||
http:
|
||||
paths:
|
||||
- path: /v1/
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: opnet-watcher
|
||||
port:
|
||||
number: 8090
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
@@ -576,3 +600,325 @@ spec:
|
||||
name: bridge-ui
|
||||
port:
|
||||
number: 80
|
||||
# SPC — white-label
|
||||
- host: bridge.spc.network
|
||||
http:
|
||||
paths:
|
||||
- path: /v1/
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: opnet-watcher
|
||||
port:
|
||||
number: 8090
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: bridge-ui
|
||||
port:
|
||||
number: 80
|
||||
---
|
||||
# Bridge Tenant ConfigMap — loaded from manifests/tenants/*.yaml
|
||||
# One key per tenant manifest. opnet-watcher reads BRIDGE_TENANT_DIR.
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: bridge-tenants
|
||||
namespace: lux-bridge
|
||||
data:
|
||||
lux.yaml: |
|
||||
id: lux
|
||||
name: "Lux Network"
|
||||
hostnames: ["bridge.lux.network", "bridge.lux.io"]
|
||||
home_chain_id: 96369
|
||||
supported_chains: [lux-c, ethereum, base, opnet, solana, ton]
|
||||
teleporter_addresses:
|
||||
"96369": "kms://teleport/lux-c/teleporter"
|
||||
"1": "kms://teleport/lux-c/teleporter-eth"
|
||||
"8453": "kms://teleport/lux-c/teleporter-eth"
|
||||
"4294967299": "kms://teleport/lux-c/teleporter-btc"
|
||||
"1399811149": "kms://teleport/lux-c/teleporter-sol"
|
||||
"1414483791": "kms://teleport/lux-c/teleporter-ton"
|
||||
branding:
|
||||
logo_url: "/static/lux/logo.svg"
|
||||
primary_color: "#E84142"
|
||||
theme: dark
|
||||
features: {swap: true, yield: true, perps: true}
|
||||
zoo.yaml: |
|
||||
id: zoo
|
||||
name: "Zoo Network"
|
||||
hostnames: ["bridge.zoo.ngo"]
|
||||
home_chain_id: 200200
|
||||
supported_chains: [lux-c, ethereum, base, solana]
|
||||
teleporter_addresses:
|
||||
"96369": "kms://teleport/lux-c/teleporter"
|
||||
"1": "kms://teleport/lux-c/teleporter-eth"
|
||||
"8453": "kms://teleport/lux-c/teleporter-eth"
|
||||
"1399811149": "kms://teleport/lux-c/teleporter-sol"
|
||||
branding:
|
||||
logo_url: "/static/zoo/logo.svg"
|
||||
primary_color: "#10B981"
|
||||
theme: dark
|
||||
features: {swap: true, yield: true, perps: false}
|
||||
hanzo.yaml: |
|
||||
id: hanzo
|
||||
name: "Hanzo Network"
|
||||
hostnames: ["bridge.hanzo.ai"]
|
||||
home_chain_id: 36963
|
||||
supported_chains: [lux-c, ethereum, base, opnet]
|
||||
teleporter_addresses:
|
||||
"96369": "kms://teleport/lux-c/teleporter"
|
||||
"1": "kms://teleport/lux-c/teleporter-eth"
|
||||
"8453": "kms://teleport/lux-c/teleporter-eth"
|
||||
"4294967299": "kms://teleport/lux-c/teleporter-btc"
|
||||
branding:
|
||||
logo_url: "/static/hanzo/logo.svg"
|
||||
primary_color: "#6366F1"
|
||||
theme: dark
|
||||
features: {swap: true, yield: true, perps: true}
|
||||
pars.yaml: |
|
||||
id: pars
|
||||
name: "Pars Network"
|
||||
hostnames: ["bridge.pars.network"]
|
||||
home_chain_id: 494949
|
||||
supported_chains: [lux-c, ethereum, base]
|
||||
teleporter_addresses:
|
||||
"96369": "kms://teleport/lux-c/teleporter"
|
||||
"1": "kms://teleport/lux-c/teleporter-eth"
|
||||
"8453": "kms://teleport/lux-c/teleporter-eth"
|
||||
branding:
|
||||
logo_url: "/static/pars/logo.svg"
|
||||
primary_color: "#F59E0B"
|
||||
theme: light
|
||||
features: {swap: true, yield: false, perps: false}
|
||||
spc.yaml: |
|
||||
id: spc
|
||||
name: "SPC Network"
|
||||
hostnames: ["bridge.spc.network"]
|
||||
home_chain_id: 36911
|
||||
supported_chains: [lux-c, ethereum, base]
|
||||
teleporter_addresses:
|
||||
"96369": "kms://teleport/lux-c/teleporter"
|
||||
"1": "kms://teleport/lux-c/teleporter-eth"
|
||||
"8453": "kms://teleport/lux-c/teleporter-eth"
|
||||
branding:
|
||||
logo_url: "/static/spc/logo.svg"
|
||||
primary_color: "#3B82F6"
|
||||
theme: dark
|
||||
features: {swap: true, yield: false, perps: false}
|
||||
---
|
||||
# Bridge Chain ConfigMap — loaded from manifests/chains/*.yaml
|
||||
# One key per chain manifest. opnet-watcher reads BRIDGE_CHAIN_DIR.
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: bridge-chains
|
||||
namespace: lux-bridge
|
||||
data:
|
||||
lux-c.yaml: |
|
||||
chain_id: 96369
|
||||
name: "Lux C-Chain"
|
||||
class: lux_native
|
||||
tier: yield_enabled
|
||||
assets:
|
||||
- ticker: LUX
|
||||
decimals: 18
|
||||
recipient_encoding: evm_address
|
||||
wrapper: {symbol: LUX, contract: "token/LUX.sol", decimals: 18}
|
||||
finality: {mode: instant, confirmations: 1}
|
||||
ethereum.yaml: |
|
||||
chain_id: 1
|
||||
name: "Ethereum"
|
||||
class: evm
|
||||
tier: yield_enabled
|
||||
assets:
|
||||
- ticker: ETH
|
||||
decimals: 18
|
||||
recipient_encoding: evm_address
|
||||
wrapper: {symbol: LETH, contract: "liquid/tokens/LETH.sol", decimals: 18}
|
||||
- ticker: USDC
|
||||
decimals: 6
|
||||
recipient_encoding: evm_address
|
||||
wrapper: {symbol: LUSD, contract: "liquid/tokens/LUSD.sol", decimals: 18}
|
||||
- ticker: USDT
|
||||
decimals: 6
|
||||
recipient_encoding: evm_address
|
||||
wrapper: {symbol: LUSD, contract: "liquid/tokens/LUSD.sol", decimals: 18}
|
||||
- ticker: DAI
|
||||
decimals: 18
|
||||
recipient_encoding: evm_address
|
||||
wrapper: {symbol: LUSD, contract: "liquid/tokens/LUSD.sol", decimals: 18}
|
||||
finality: {mode: slot_finalized, confirmations: 64}
|
||||
base.yaml: |
|
||||
chain_id: 8453
|
||||
name: "Base"
|
||||
class: evm
|
||||
tier: yield_enabled
|
||||
assets:
|
||||
- ticker: ETH
|
||||
decimals: 18
|
||||
recipient_encoding: evm_address
|
||||
wrapper: {symbol: LETH, contract: "liquid/tokens/LETH.sol", decimals: 18}
|
||||
- ticker: USDC
|
||||
decimals: 6
|
||||
recipient_encoding: evm_address
|
||||
wrapper: {symbol: LUSD, contract: "liquid/tokens/LUSD.sol", decimals: 18}
|
||||
finality: {mode: block_depth, confirmations: 12}
|
||||
opnet.yaml: |
|
||||
chain_id: 4294967299
|
||||
name: "OP_NET"
|
||||
class: bitcoin_script
|
||||
tier: yield_enabled
|
||||
assets:
|
||||
- ticker: BTC
|
||||
decimals: 8
|
||||
recipient_encoding: bech32
|
||||
wrapper: {symbol: LBTC, contract: "liquid/tokens/LBTC.sol", decimals: 18}
|
||||
finality: {mode: block_depth, confirmations: 6}
|
||||
solana.yaml: |
|
||||
chain_id: 1399811149
|
||||
name: "Solana"
|
||||
class: account_native
|
||||
tier: locale_only
|
||||
assets:
|
||||
- ticker: SOL
|
||||
decimals: 9
|
||||
recipient_encoding: base58
|
||||
wrapper: {symbol: LSOL, contract: "liquid/tokens/LSOL.sol", decimals: 18}
|
||||
finality: {mode: slot_finalized, confirmations: 32}
|
||||
ton.yaml: |
|
||||
chain_id: 1414483791
|
||||
name: "TON"
|
||||
class: account_native
|
||||
tier: locale_only
|
||||
assets:
|
||||
- ticker: TON
|
||||
decimals: 9
|
||||
recipient_encoding: raw_bytes
|
||||
wrapper: {symbol: LTON, contract: "liquid/tokens/LTON.sol", decimals: 18}
|
||||
finality: {mode: block_depth, confirmations: 1}
|
||||
---
|
||||
# OPNET Watcher ConfigMap
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: opnet-watcher-config
|
||||
namespace: lux-bridge
|
||||
data:
|
||||
config.yaml: |
|
||||
lux_rpc: https://api.lux.network/ext/bc/C/rpc
|
||||
zoo_rpc: https://api.zoo.network/ext/bc/C/rpc
|
||||
opnet_rpc: https://opnet.lux.network/rpc
|
||||
teleporter_address: ""
|
||||
lux_chain_id: 96369
|
||||
zoo_chain_id: 200200
|
||||
opnet_chain_id: 4294967299
|
||||
poll_interval_seconds: 12
|
||||
confirmations: 6
|
||||
health_port: 8080
|
||||
---
|
||||
# OPNET Watcher Deployment
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: opnet-watcher
|
||||
namespace: lux-bridge
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: opnet-watcher
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: opnet-watcher
|
||||
spec:
|
||||
containers:
|
||||
- name: opnet-watcher
|
||||
image: ghcr.io/luxfi/opnet-watcher:latest
|
||||
args: ["--config", "/config/config.yaml"]
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
name: health
|
||||
- containerPort: 8090
|
||||
name: api
|
||||
env:
|
||||
- name: MPC_API_URL
|
||||
value: http://mpc-api-svc.lux-bridge.svc:8081
|
||||
- name: MPC_API_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: bridge-secrets
|
||||
key: mpc-api-token
|
||||
- name: KMS_KEY_SHARES
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: opnet-watcher-secrets
|
||||
key: kms-key-shares
|
||||
- name: BRIDGE_TENANT_DIR
|
||||
value: /etc/bridge/tenants
|
||||
- name: BRIDGE_CHAIN_DIR
|
||||
value: /etc/bridge/chains
|
||||
volumeMounts:
|
||||
- name: config
|
||||
mountPath: /config
|
||||
readOnly: true
|
||||
- name: tenants
|
||||
mountPath: /etc/bridge/tenants
|
||||
readOnly: true
|
||||
- name: chains
|
||||
mountPath: /etc/bridge/chains
|
||||
readOnly: true
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 8090
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 15
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 8090
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
resources:
|
||||
requests:
|
||||
memory: "128Mi"
|
||||
cpu: "100m"
|
||||
limits:
|
||||
memory: "256Mi"
|
||||
cpu: "250m"
|
||||
volumes:
|
||||
- name: config
|
||||
configMap:
|
||||
name: opnet-watcher-config
|
||||
- name: tenants
|
||||
configMap:
|
||||
name: bridge-tenants
|
||||
- name: chains
|
||||
configMap:
|
||||
name: bridge-chains
|
||||
---
|
||||
# opnet-watcher-secrets is managed externally via KMS (kms.hanzo.ai).
|
||||
# Apply with:
|
||||
# kubectl create secret generic opnet-watcher-secrets -n lux-bridge \
|
||||
# --from-literal="kms-key-shares=$(kms get OPNET_WATCHER_KMS_KEY_SHARES)" \
|
||||
# --dry-run=client -o yaml | kubectl apply -f -
|
||||
# DO NOT add secret values here -- never commit keys to git.
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: opnet-watcher
|
||||
namespace: lux-bridge
|
||||
spec:
|
||||
selector:
|
||||
app: opnet-watcher
|
||||
ports:
|
||||
- port: 8080
|
||||
targetPort: 8080
|
||||
name: health
|
||||
- port: 8090
|
||||
targetPort: 8090
|
||||
name: api
|
||||
@@ -0,0 +1,42 @@
|
||||
# Algorand — Teleport Chain Manifest
|
||||
# Schema: ../schema.yaml v1.0.0
|
||||
#
|
||||
# Status: locale_only — FROST Ed25519 signer integration pending.
|
||||
|
||||
chain_id: 4294967450
|
||||
name: "Algorand"
|
||||
class: account_native
|
||||
tier: locale_only
|
||||
|
||||
signer:
|
||||
protocol: frost
|
||||
curve: ed25519
|
||||
threshold:
|
||||
t: 2
|
||||
n: 3
|
||||
|
||||
finality:
|
||||
mode: slot_finalized # Algorand instant finality via Pure PoS
|
||||
confirmations: 1
|
||||
max_reorg_depth: 0
|
||||
stale_attestation_seconds: 86400
|
||||
|
||||
assets:
|
||||
- ticker: ALGO
|
||||
decimals: 6
|
||||
recipient_encoding: base58 # Algorand base32/base58 addresses
|
||||
wrapper:
|
||||
symbol: LALGO
|
||||
contract: "liquid/tokens/LALGO.sol"
|
||||
decimals: 18
|
||||
|
||||
adapter:
|
||||
deposit_method: account_watch
|
||||
vault_contract: "kms://teleport/algorand/mpc-address"
|
||||
teleporter_contract: "kms://teleport/lux-c/teleporter-algo"
|
||||
|
||||
watcher:
|
||||
plugin: algorand_indexer_watcher
|
||||
rpc_url: "kms://teleport/algorand/rpc"
|
||||
poll_interval_ms: 3300
|
||||
start_block: 0
|
||||
@@ -0,0 +1,42 @@
|
||||
# Aptos — Teleport Chain Manifest
|
||||
# Schema: ../schema.yaml v1.0.0
|
||||
#
|
||||
# Status: locale_only — FROST Ed25519 signer integration pending.
|
||||
|
||||
chain_id: 4294967340
|
||||
name: "Aptos"
|
||||
class: account_native
|
||||
tier: locale_only
|
||||
|
||||
signer:
|
||||
protocol: frost
|
||||
curve: ed25519
|
||||
threshold:
|
||||
t: 2
|
||||
n: 3
|
||||
|
||||
finality:
|
||||
mode: slot_finalized # Aptos BFT finality per block
|
||||
confirmations: 1
|
||||
max_reorg_depth: 0
|
||||
stale_attestation_seconds: 86400
|
||||
|
||||
assets:
|
||||
- ticker: APT
|
||||
decimals: 8
|
||||
recipient_encoding: raw_bytes # 32-byte Aptos address
|
||||
wrapper:
|
||||
symbol: LAPT
|
||||
contract: "liquid/tokens/LAPT.sol"
|
||||
decimals: 18
|
||||
|
||||
adapter:
|
||||
deposit_method: account_watch
|
||||
vault_contract: "kms://teleport/aptos/mpc-address"
|
||||
teleporter_contract: "kms://teleport/lux-c/teleporter-apt"
|
||||
|
||||
watcher:
|
||||
plugin: aptos_ws_watcher
|
||||
rpc_url: "kms://teleport/aptos/rpc"
|
||||
poll_interval_ms: 1000
|
||||
start_block: 0
|
||||
@@ -0,0 +1,48 @@
|
||||
# Arbitrum One — Teleport Chain Manifest
|
||||
# Schema: ../schema.yaml v1.0.0
|
||||
|
||||
chain_id: 42161
|
||||
name: "Arbitrum"
|
||||
class: evm
|
||||
tier: bidirectional
|
||||
|
||||
signer:
|
||||
protocol: cggmp21
|
||||
curve: secp256k1
|
||||
threshold:
|
||||
t: 2
|
||||
n: 3
|
||||
|
||||
finality:
|
||||
mode: block_depth
|
||||
confirmations: 64 # L2 blocks, ~16s; wait for L1 batch confirmation
|
||||
max_reorg_depth: 128
|
||||
stale_attestation_seconds: 86400
|
||||
|
||||
assets:
|
||||
- ticker: ETH
|
||||
decimals: 18
|
||||
recipient_encoding: evm_address
|
||||
wrapper:
|
||||
symbol: LETH
|
||||
contract: "liquid/tokens/LETH.sol"
|
||||
decimals: 18
|
||||
|
||||
- ticker: ARB
|
||||
decimals: 18
|
||||
recipient_encoding: evm_address
|
||||
wrapper:
|
||||
symbol: LARB
|
||||
contract: "liquid/tokens/LARB.sol"
|
||||
decimals: 18
|
||||
|
||||
adapter:
|
||||
deposit_method: event_log
|
||||
vault_contract: "kms://teleport/arbitrum/teleport-vault"
|
||||
teleporter_contract: "kms://teleport/lux-c/teleporter-arb"
|
||||
|
||||
watcher:
|
||||
plugin: evm_log_watcher
|
||||
rpc_url: "kms://teleport/arbitrum/rpc"
|
||||
poll_interval_ms: 250
|
||||
start_block: 0
|
||||
@@ -0,0 +1,40 @@
|
||||
# Avalanche C-Chain — Teleport Chain Manifest
|
||||
# Schema: ../schema.yaml v1.0.0
|
||||
|
||||
chain_id: 43114
|
||||
name: "Avalanche"
|
||||
class: evm
|
||||
tier: bidirectional
|
||||
|
||||
signer:
|
||||
protocol: cggmp21
|
||||
curve: secp256k1
|
||||
threshold:
|
||||
t: 2
|
||||
n: 3
|
||||
|
||||
finality:
|
||||
mode: block_depth
|
||||
confirmations: 35
|
||||
max_reorg_depth: 64
|
||||
stale_attestation_seconds: 86400
|
||||
|
||||
assets:
|
||||
- ticker: AVAX
|
||||
decimals: 18
|
||||
recipient_encoding: evm_address
|
||||
wrapper:
|
||||
symbol: LAVAX
|
||||
contract: "liquid/tokens/LAVAX.sol"
|
||||
decimals: 18
|
||||
|
||||
adapter:
|
||||
deposit_method: event_log
|
||||
vault_contract: "kms://teleport/avalanche/teleport-vault"
|
||||
teleporter_contract: "kms://teleport/lux-c/teleporter-avax"
|
||||
|
||||
watcher:
|
||||
plugin: evm_log_watcher
|
||||
rpc_url: "kms://teleport/avalanche/rpc"
|
||||
poll_interval_ms: 2000
|
||||
start_block: 0
|
||||
@@ -0,0 +1,51 @@
|
||||
# Base (Coinbase L2) — Teleport Chain Manifest
|
||||
# Schema: ../schema.yaml v1.0.0
|
||||
|
||||
chain_id: 8453
|
||||
name: "Base"
|
||||
class: evm
|
||||
tier: yield_enabled
|
||||
|
||||
signer:
|
||||
protocol: cggmp21
|
||||
curve: secp256k1
|
||||
threshold:
|
||||
t: 2
|
||||
n: 3
|
||||
|
||||
finality:
|
||||
mode: block_depth
|
||||
confirmations: 12 # L2 blocks, ~24s
|
||||
max_reorg_depth: 64
|
||||
stale_attestation_seconds: 86400
|
||||
|
||||
assets:
|
||||
- ticker: ETH
|
||||
decimals: 18
|
||||
recipient_encoding: evm_address
|
||||
wrapper:
|
||||
symbol: LETH
|
||||
contract: "liquid/tokens/LETH.sol"
|
||||
decimals: 18
|
||||
strategy:
|
||||
name: AaveV3Strategy
|
||||
contract: "bridge/teleport/strategies/AaveV3Strategy.sol"
|
||||
|
||||
- ticker: USDC
|
||||
decimals: 6
|
||||
recipient_encoding: evm_address
|
||||
wrapper:
|
||||
symbol: LUSD
|
||||
contract: "liquid/tokens/LUSD.sol"
|
||||
decimals: 18
|
||||
|
||||
adapter:
|
||||
deposit_method: event_log
|
||||
vault_contract: "kms://teleport/base/liquid-vault"
|
||||
teleporter_contract: "kms://teleport/lux-c/teleporter-eth"
|
||||
|
||||
watcher:
|
||||
plugin: evm_log_watcher
|
||||
rpc_url: "kms://teleport/base/rpc"
|
||||
poll_interval_ms: 2000 # ~2s Base block time
|
||||
start_block: 0
|
||||
@@ -0,0 +1,48 @@
|
||||
# BNB Smart Chain — Teleport Chain Manifest
|
||||
# Schema: ../schema.yaml v1.0.0
|
||||
|
||||
chain_id: 56
|
||||
name: "BNB Smart Chain"
|
||||
class: evm
|
||||
tier: bidirectional
|
||||
|
||||
signer:
|
||||
protocol: cggmp21
|
||||
curve: secp256k1
|
||||
threshold:
|
||||
t: 2
|
||||
n: 3
|
||||
|
||||
finality:
|
||||
mode: block_depth
|
||||
confirmations: 15 # ~45s at 3s block time
|
||||
max_reorg_depth: 64
|
||||
stale_attestation_seconds: 86400
|
||||
|
||||
assets:
|
||||
- ticker: BNB
|
||||
decimals: 18
|
||||
recipient_encoding: evm_address
|
||||
wrapper:
|
||||
symbol: LBNB
|
||||
contract: "liquid/tokens/LBNB.sol"
|
||||
decimals: 18
|
||||
|
||||
- ticker: USDT
|
||||
decimals: 18
|
||||
recipient_encoding: evm_address
|
||||
wrapper:
|
||||
symbol: LUSD
|
||||
contract: "liquid/tokens/LUSD.sol"
|
||||
decimals: 18
|
||||
|
||||
adapter:
|
||||
deposit_method: event_log
|
||||
vault_contract: "kms://teleport/bsc/teleport-vault"
|
||||
teleporter_contract: "kms://teleport/lux-c/teleporter-bnb"
|
||||
|
||||
watcher:
|
||||
plugin: evm_log_watcher
|
||||
rpc_url: "kms://teleport/bsc/rpc"
|
||||
poll_interval_ms: 3000
|
||||
start_block: 0
|
||||
@@ -0,0 +1,43 @@
|
||||
# Cardano — Teleport Chain Manifest
|
||||
# Schema: ../schema.yaml v1.0.0
|
||||
#
|
||||
# Status: locale_only — FROST Ed25519 signer integration pending.
|
||||
# Note: Cardano uses extended UTXO model with Ed25519-BIP32 keys.
|
||||
|
||||
chain_id: 4294967430
|
||||
name: "Cardano"
|
||||
class: account_native
|
||||
tier: locale_only
|
||||
|
||||
signer:
|
||||
protocol: frost
|
||||
curve: ed25519
|
||||
threshold:
|
||||
t: 2
|
||||
n: 3
|
||||
|
||||
finality:
|
||||
mode: block_depth
|
||||
confirmations: 15 # ~5 minutes at 20s block time for safe settlement
|
||||
max_reorg_depth: 36 # Ouroboros rollback limit (k parameter)
|
||||
stale_attestation_seconds: 86400
|
||||
|
||||
assets:
|
||||
- ticker: ADA
|
||||
decimals: 6
|
||||
recipient_encoding: bech32 # addr1... bech32 Shelley addresses
|
||||
wrapper:
|
||||
symbol: LADA
|
||||
contract: "liquid/tokens/LADA.sol"
|
||||
decimals: 18
|
||||
|
||||
adapter:
|
||||
deposit_method: account_watch
|
||||
vault_contract: "kms://teleport/cardano/mpc-address"
|
||||
teleporter_contract: "kms://teleport/lux-c/teleporter-ada"
|
||||
|
||||
watcher:
|
||||
plugin: cardano_ogmios_watcher
|
||||
rpc_url: "kms://teleport/cardano/ogmios"
|
||||
poll_interval_ms: 20000
|
||||
start_block: 0
|
||||
@@ -0,0 +1,42 @@
|
||||
# Cosmos Hub — Teleport Chain Manifest
|
||||
# Schema: ../schema.yaml v1.0.0
|
||||
#
|
||||
# Status: locale_only — CGGMP21 secp256k1 adapter integration pending.
|
||||
|
||||
chain_id: 4294967350
|
||||
name: "Cosmos Hub"
|
||||
class: account_native
|
||||
tier: locale_only
|
||||
|
||||
signer:
|
||||
protocol: cggmp21
|
||||
curve: secp256k1
|
||||
threshold:
|
||||
t: 2
|
||||
n: 3
|
||||
|
||||
finality:
|
||||
mode: slot_finalized # CometBFT instant finality per block
|
||||
confirmations: 1
|
||||
max_reorg_depth: 0
|
||||
stale_attestation_seconds: 86400
|
||||
|
||||
assets:
|
||||
- ticker: ATOM
|
||||
decimals: 6
|
||||
recipient_encoding: bech32 # cosmos1... bech32 addresses
|
||||
wrapper:
|
||||
symbol: LATOM
|
||||
contract: "liquid/tokens/LATOM.sol"
|
||||
decimals: 18
|
||||
|
||||
adapter:
|
||||
deposit_method: account_watch
|
||||
vault_contract: "kms://teleport/cosmos/mpc-address"
|
||||
teleporter_contract: "kms://teleport/lux-c/teleporter-atom"
|
||||
|
||||
watcher:
|
||||
plugin: cosmos_ws_watcher
|
||||
rpc_url: "kms://teleport/cosmos/rpc"
|
||||
poll_interval_ms: 6000
|
||||
start_block: 0
|
||||
@@ -0,0 +1,67 @@
|
||||
# Ethereum Mainnet — Teleport Chain Manifest
|
||||
# Schema: ../schema.yaml v1.0.0
|
||||
|
||||
chain_id: 1
|
||||
name: "Ethereum"
|
||||
class: evm
|
||||
tier: yield_enabled
|
||||
|
||||
signer:
|
||||
protocol: cggmp21
|
||||
curve: secp256k1
|
||||
threshold:
|
||||
t: 2
|
||||
n: 3
|
||||
|
||||
finality:
|
||||
mode: slot_finalized
|
||||
confirmations: 64 # ~2 epochs for safe finality
|
||||
max_reorg_depth: 128
|
||||
stale_attestation_seconds: 86400
|
||||
|
||||
assets:
|
||||
- ticker: ETH
|
||||
decimals: 18
|
||||
recipient_encoding: evm_address
|
||||
wrapper:
|
||||
symbol: LETH
|
||||
contract: "liquid/tokens/LETH.sol"
|
||||
decimals: 18
|
||||
strategy:
|
||||
name: LidoStrategy
|
||||
contract: "bridge/teleport/strategies/LidoStrategy.sol"
|
||||
|
||||
- ticker: USDC
|
||||
decimals: 6
|
||||
recipient_encoding: evm_address
|
||||
wrapper:
|
||||
symbol: LUSD
|
||||
contract: "liquid/tokens/LUSD.sol"
|
||||
decimals: 18
|
||||
|
||||
- ticker: USDT
|
||||
decimals: 6
|
||||
recipient_encoding: evm_address
|
||||
wrapper:
|
||||
symbol: LUSD
|
||||
contract: "liquid/tokens/LUSD.sol"
|
||||
decimals: 18
|
||||
|
||||
- ticker: DAI
|
||||
decimals: 18
|
||||
recipient_encoding: evm_address
|
||||
wrapper:
|
||||
symbol: LUSD
|
||||
contract: "liquid/tokens/LUSD.sol"
|
||||
decimals: 18
|
||||
|
||||
adapter:
|
||||
deposit_method: event_log
|
||||
vault_contract: "kms://teleport/ethereum/liquid-vault"
|
||||
teleporter_contract: "kms://teleport/lux-c/teleporter-eth"
|
||||
|
||||
watcher:
|
||||
plugin: evm_log_watcher
|
||||
rpc_url: "kms://teleport/ethereum/rpc"
|
||||
poll_interval_ms: 12000
|
||||
start_block: 0
|
||||
@@ -0,0 +1,43 @@
|
||||
# Filecoin (FEVM) — Teleport Chain Manifest
|
||||
# Schema: ../schema.yaml v1.0.0
|
||||
#
|
||||
# Status: locale_only — EVM adapter integration pending.
|
||||
# Filecoin EVM (FEVM) provides Solidity-compatible smart contracts.
|
||||
|
||||
chain_id: 314
|
||||
name: "Filecoin"
|
||||
class: evm
|
||||
tier: locale_only
|
||||
|
||||
signer:
|
||||
protocol: cggmp21
|
||||
curve: secp256k1
|
||||
threshold:
|
||||
t: 2
|
||||
n: 3
|
||||
|
||||
finality:
|
||||
mode: block_depth
|
||||
confirmations: 900 # Filecoin finality ~7.5 hours (900 tipsets at 30s)
|
||||
max_reorg_depth: 900
|
||||
stale_attestation_seconds: 86400
|
||||
|
||||
assets:
|
||||
- ticker: FIL
|
||||
decimals: 18
|
||||
recipient_encoding: evm_address
|
||||
wrapper:
|
||||
symbol: LFIL
|
||||
contract: "liquid/tokens/LFIL.sol"
|
||||
decimals: 18
|
||||
|
||||
adapter:
|
||||
deposit_method: event_log
|
||||
vault_contract: "kms://teleport/filecoin/teleport-vault"
|
||||
teleporter_contract: "kms://teleport/lux-c/teleporter-fil"
|
||||
|
||||
watcher:
|
||||
plugin: evm_log_watcher
|
||||
rpc_url: "kms://teleport/filecoin/rpc"
|
||||
poll_interval_ms: 30000
|
||||
start_block: 0
|
||||
@@ -0,0 +1,43 @@
|
||||
# Fuel — Teleport Chain Manifest
|
||||
# Schema: ../schema.yaml v1.0.0
|
||||
#
|
||||
# Status: locale_only — FuelVM (Sway) adapter pending.
|
||||
# Fuel uses UTXO-based parallel execution with Sway smart contracts.
|
||||
|
||||
chain_id: 4294967520 # FUEL_MAINNET from ChainIds.sol
|
||||
name: "Fuel"
|
||||
class: account_native
|
||||
tier: locale_only
|
||||
|
||||
signer:
|
||||
protocol: frost
|
||||
curve: ed25519
|
||||
threshold:
|
||||
t: 2
|
||||
n: 3
|
||||
|
||||
finality:
|
||||
mode: slot_finalized # Fuel settles to Ethereum DA
|
||||
confirmations: 1
|
||||
max_reorg_depth: 8
|
||||
stale_attestation_seconds: 86400
|
||||
|
||||
assets:
|
||||
- ticker: ETH
|
||||
decimals: 9
|
||||
recipient_encoding: raw_bytes # 32-byte Fuel (Bech32m) address
|
||||
wrapper:
|
||||
symbol: LETH
|
||||
contract: "liquid/tokens/LETH.sol"
|
||||
decimals: 18
|
||||
|
||||
adapter:
|
||||
deposit_method: account_watch
|
||||
vault_contract: "kms://teleport/fuel/mpc-address"
|
||||
teleporter_contract: "kms://teleport/lux-c/teleporter-fuel"
|
||||
|
||||
watcher:
|
||||
plugin: fuel_ws_watcher
|
||||
rpc_url: "kms://teleport/fuel/rpc"
|
||||
poll_interval_ms: 1000
|
||||
start_block: 0
|
||||
@@ -0,0 +1,42 @@
|
||||
# Hyperliquid — Teleport Chain Manifest
|
||||
# Schema: ../schema.yaml v1.0.0
|
||||
#
|
||||
# Status: locale_only — EVM adapter integration pending.
|
||||
|
||||
chain_id: 999
|
||||
name: "Hyperliquid"
|
||||
class: evm
|
||||
tier: locale_only
|
||||
|
||||
signer:
|
||||
protocol: cggmp21
|
||||
curve: secp256k1
|
||||
threshold:
|
||||
t: 2
|
||||
n: 3
|
||||
|
||||
finality:
|
||||
mode: block_depth
|
||||
confirmations: 10
|
||||
max_reorg_depth: 32
|
||||
stale_attestation_seconds: 86400
|
||||
|
||||
assets:
|
||||
- ticker: HYPE
|
||||
decimals: 18
|
||||
recipient_encoding: evm_address
|
||||
wrapper:
|
||||
symbol: LHYPE
|
||||
contract: "liquid/tokens/LHYPE.sol"
|
||||
decimals: 18
|
||||
|
||||
adapter:
|
||||
deposit_method: event_log
|
||||
vault_contract: "kms://teleport/hyperliquid/teleport-vault"
|
||||
teleporter_contract: "kms://teleport/lux-c/teleporter-hype"
|
||||
|
||||
watcher:
|
||||
plugin: evm_log_watcher
|
||||
rpc_url: "kms://teleport/hyperliquid/rpc"
|
||||
poll_interval_ms: 2000
|
||||
start_block: 0
|
||||
@@ -0,0 +1,42 @@
|
||||
# Internet Computer (ICP) — Teleport Chain Manifest
|
||||
# Schema: ../schema.yaml v1.0.0
|
||||
#
|
||||
# Status: locale_only — CGGMP21 secp256k1 via canister threshold ECDSA pending.
|
||||
|
||||
chain_id: 4294967490
|
||||
name: "Internet Computer"
|
||||
class: account_native
|
||||
tier: locale_only
|
||||
|
||||
signer:
|
||||
protocol: cggmp21
|
||||
curve: secp256k1 # ICP chain-key ECDSA (threshold secp256k1)
|
||||
threshold:
|
||||
t: 2
|
||||
n: 3
|
||||
|
||||
finality:
|
||||
mode: slot_finalized # ICP consensus — finality per round (~1s)
|
||||
confirmations: 2
|
||||
max_reorg_depth: 0
|
||||
stale_attestation_seconds: 86400
|
||||
|
||||
assets:
|
||||
- ticker: ICP
|
||||
decimals: 8
|
||||
recipient_encoding: raw_bytes # ICP principal / account identifier
|
||||
wrapper:
|
||||
symbol: LICP
|
||||
contract: "liquid/tokens/LICP.sol"
|
||||
decimals: 18
|
||||
|
||||
adapter:
|
||||
deposit_method: account_watch
|
||||
vault_contract: "kms://teleport/icp/mpc-canister"
|
||||
teleporter_contract: "kms://teleport/lux-c/teleporter-icp"
|
||||
|
||||
watcher:
|
||||
plugin: icp_agent_watcher
|
||||
rpc_url: "kms://teleport/icp/agent"
|
||||
poll_interval_ms: 2000
|
||||
start_block: 0
|
||||
@@ -0,0 +1,46 @@
|
||||
# Lux C-Chain — Teleport Chain Manifest
|
||||
# Schema: ../schema.yaml v1.0.0
|
||||
#
|
||||
# Lux C-Chain is the destination for all Teleport minting, and also serves
|
||||
# as a source chain for bridging LUX to other networks.
|
||||
|
||||
chain_id: 96369
|
||||
name: "Lux C-Chain"
|
||||
class: lux_native
|
||||
tier: yield_enabled
|
||||
|
||||
signer:
|
||||
protocol: bls
|
||||
curve: bls12_381
|
||||
threshold:
|
||||
t: 2
|
||||
n: 3
|
||||
native_signer:
|
||||
protocol: bls
|
||||
curve: bls12_381 # Warp messaging uses BLS aggregation
|
||||
|
||||
finality:
|
||||
mode: instant # Snowman consensus — single-slot finality
|
||||
confirmations: 1
|
||||
max_reorg_depth: 0 # No reorgs under BFT
|
||||
stale_attestation_seconds: 86400
|
||||
|
||||
assets:
|
||||
- ticker: LUX
|
||||
decimals: 18
|
||||
recipient_encoding: evm_address
|
||||
wrapper:
|
||||
symbol: LUX
|
||||
contract: "token/LUX.sol"
|
||||
decimals: 18
|
||||
|
||||
adapter:
|
||||
deposit_method: event_log
|
||||
vault_contract: "kms://teleport/lux-c/native-vault"
|
||||
teleporter_contract: "kms://teleport/lux-c/teleporter"
|
||||
|
||||
watcher:
|
||||
plugin: lux_warp_watcher
|
||||
rpc_url: "https://api.lux.network/ext/bc/C/rpc"
|
||||
poll_interval_ms: 2000 # ~2s C-Chain block time
|
||||
start_block: 0
|
||||
@@ -0,0 +1,48 @@
|
||||
# Lux Hanzo Chain — Teleport Chain Manifest
|
||||
# Schema: ../schema.yaml v1.0.0
|
||||
#
|
||||
# Hanzo AI L1 subnet on Lux. Uses Warp messaging for native bridging.
|
||||
|
||||
chain_id: 36963
|
||||
name: "Lux Hanzo"
|
||||
class: lux_native
|
||||
tier: yield_enabled
|
||||
|
||||
signer:
|
||||
protocol: bls
|
||||
curve: bls12_381
|
||||
threshold:
|
||||
t: 2
|
||||
n: 3
|
||||
native_signer:
|
||||
protocol: bls
|
||||
curve: bls12_381
|
||||
|
||||
finality:
|
||||
mode: instant
|
||||
confirmations: 1
|
||||
max_reorg_depth: 0
|
||||
stale_attestation_seconds: 86400
|
||||
|
||||
assets:
|
||||
- ticker: HANZO
|
||||
decimals: 18
|
||||
recipient_encoding: evm_address
|
||||
wrapper:
|
||||
symbol: LHANZO
|
||||
contract: "liquid/tokens/LHANZO.sol"
|
||||
decimals: 18
|
||||
strategy:
|
||||
name: HanzoStakingStrategy
|
||||
contract: "bridge/teleport/strategies/HanzoStakingStrategy.sol"
|
||||
|
||||
adapter:
|
||||
deposit_method: event_log
|
||||
vault_contract: "kms://teleport/lux-hanzo/native-vault"
|
||||
teleporter_contract: "kms://teleport/lux-c/teleporter-hanzo"
|
||||
|
||||
watcher:
|
||||
plugin: lux_warp_watcher
|
||||
rpc_url: "kms://teleport/lux-hanzo/rpc"
|
||||
poll_interval_ms: 2000
|
||||
start_block: 0
|
||||
@@ -0,0 +1,45 @@
|
||||
# Lux Pars Chain — Teleport Chain Manifest
|
||||
# Schema: ../schema.yaml v1.0.0
|
||||
#
|
||||
# Pars L1 subnet on Lux. Uses Warp messaging for native bridging.
|
||||
|
||||
chain_id: 494949
|
||||
name: "Lux Pars"
|
||||
class: lux_native
|
||||
tier: bidirectional
|
||||
|
||||
signer:
|
||||
protocol: bls
|
||||
curve: bls12_381
|
||||
threshold:
|
||||
t: 2
|
||||
n: 3
|
||||
native_signer:
|
||||
protocol: bls
|
||||
curve: bls12_381
|
||||
|
||||
finality:
|
||||
mode: instant
|
||||
confirmations: 1
|
||||
max_reorg_depth: 0
|
||||
stale_attestation_seconds: 86400
|
||||
|
||||
assets:
|
||||
- ticker: PARS
|
||||
decimals: 18
|
||||
recipient_encoding: evm_address
|
||||
wrapper:
|
||||
symbol: LPARS
|
||||
contract: "liquid/tokens/LPARS.sol"
|
||||
decimals: 18
|
||||
|
||||
adapter:
|
||||
deposit_method: event_log
|
||||
vault_contract: "kms://teleport/lux-pars/native-vault"
|
||||
teleporter_contract: "kms://teleport/lux-c/teleporter-pars"
|
||||
|
||||
watcher:
|
||||
plugin: lux_warp_watcher
|
||||
rpc_url: "kms://teleport/lux-pars/rpc"
|
||||
poll_interval_ms: 2000
|
||||
start_block: 0
|
||||
@@ -0,0 +1,45 @@
|
||||
# Lux SPC Chain — Teleport Chain Manifest
|
||||
# Schema: ../schema.yaml v1.0.0
|
||||
#
|
||||
# SPC L1 subnet on Lux. Uses Warp messaging for native bridging.
|
||||
|
||||
chain_id: 36911
|
||||
name: "Lux SPC"
|
||||
class: lux_native
|
||||
tier: bidirectional
|
||||
|
||||
signer:
|
||||
protocol: bls
|
||||
curve: bls12_381
|
||||
threshold:
|
||||
t: 2
|
||||
n: 3
|
||||
native_signer:
|
||||
protocol: bls
|
||||
curve: bls12_381
|
||||
|
||||
finality:
|
||||
mode: instant
|
||||
confirmations: 1
|
||||
max_reorg_depth: 0
|
||||
stale_attestation_seconds: 86400
|
||||
|
||||
assets:
|
||||
- ticker: SPC
|
||||
decimals: 18
|
||||
recipient_encoding: evm_address
|
||||
wrapper:
|
||||
symbol: LSPC
|
||||
contract: "liquid/tokens/LSPC.sol"
|
||||
decimals: 18
|
||||
|
||||
adapter:
|
||||
deposit_method: event_log
|
||||
vault_contract: "kms://teleport/lux-spc/native-vault"
|
||||
teleporter_contract: "kms://teleport/lux-c/teleporter-spc"
|
||||
|
||||
watcher:
|
||||
plugin: lux_warp_watcher
|
||||
rpc_url: "kms://teleport/lux-spc/rpc"
|
||||
poll_interval_ms: 2000
|
||||
start_block: 0
|
||||
@@ -0,0 +1,48 @@
|
||||
# Lux Zoo Chain — Teleport Chain Manifest
|
||||
# Schema: ../schema.yaml v1.0.0
|
||||
#
|
||||
# Zoo L1 subnet on Lux. Uses Warp messaging for native bridging.
|
||||
|
||||
chain_id: 200200
|
||||
name: "Lux Zoo"
|
||||
class: lux_native
|
||||
tier: yield_enabled
|
||||
|
||||
signer:
|
||||
protocol: bls
|
||||
curve: bls12_381
|
||||
threshold:
|
||||
t: 2
|
||||
n: 3
|
||||
native_signer:
|
||||
protocol: bls
|
||||
curve: bls12_381
|
||||
|
||||
finality:
|
||||
mode: instant
|
||||
confirmations: 1
|
||||
max_reorg_depth: 0
|
||||
stale_attestation_seconds: 86400
|
||||
|
||||
assets:
|
||||
- ticker: ZOO
|
||||
decimals: 18
|
||||
recipient_encoding: evm_address
|
||||
wrapper:
|
||||
symbol: LZOO
|
||||
contract: "liquid/tokens/LZOO.sol"
|
||||
decimals: 18
|
||||
strategy:
|
||||
name: ZooStakingStrategy
|
||||
contract: "bridge/teleport/strategies/ZooStakingStrategy.sol"
|
||||
|
||||
adapter:
|
||||
deposit_method: event_log
|
||||
vault_contract: "kms://teleport/lux-zoo/native-vault"
|
||||
teleporter_contract: "kms://teleport/lux-c/teleporter-zoo"
|
||||
|
||||
watcher:
|
||||
plugin: lux_warp_watcher
|
||||
rpc_url: "kms://teleport/lux-zoo/rpc"
|
||||
poll_interval_ms: 2000
|
||||
start_block: 0
|
||||
@@ -0,0 +1,42 @@
|
||||
# NEAR Protocol — Teleport Chain Manifest
|
||||
# Schema: ../schema.yaml v1.0.0
|
||||
#
|
||||
# Status: locale_only — FROST Ed25519 signer integration pending.
|
||||
|
||||
chain_id: 4294967400
|
||||
name: "NEAR"
|
||||
class: account_native
|
||||
tier: locale_only
|
||||
|
||||
signer:
|
||||
protocol: frost
|
||||
curve: ed25519
|
||||
threshold:
|
||||
t: 2
|
||||
n: 3
|
||||
|
||||
finality:
|
||||
mode: slot_finalized # NEAR Doomslug + Nightshade finality
|
||||
confirmations: 3
|
||||
max_reorg_depth: 8
|
||||
stale_attestation_seconds: 86400
|
||||
|
||||
assets:
|
||||
- ticker: NEAR
|
||||
decimals: 24
|
||||
recipient_encoding: raw_bytes # NEAR named accounts or implicit 64-hex
|
||||
wrapper:
|
||||
symbol: LNEAR
|
||||
contract: "liquid/tokens/LNEAR.sol"
|
||||
decimals: 18
|
||||
|
||||
adapter:
|
||||
deposit_method: account_watch
|
||||
vault_contract: "kms://teleport/near/mpc-account"
|
||||
teleporter_contract: "kms://teleport/lux-c/teleporter-near"
|
||||
|
||||
watcher:
|
||||
plugin: near_ws_watcher
|
||||
rpc_url: "kms://teleport/near/rpc"
|
||||
poll_interval_ms: 1000
|
||||
start_block: 0
|
||||
@@ -0,0 +1,50 @@
|
||||
# OP_NET (Bitcoin L1) — Teleport Chain Manifest
|
||||
# Schema: ../schema.yaml v1.0.0
|
||||
#
|
||||
# OP_NET is a Bitcoin metaprotocol layer providing smart contract capabilities
|
||||
# on Bitcoin L1. Deposits are native BTC sent to MPC-controlled Taproot UTXOs.
|
||||
# The watcher monitors Bitcoin blocks via ZMQ for confirmed deposits.
|
||||
|
||||
chain_id: 4294967299 # 0x100000003
|
||||
name: "OP_NET"
|
||||
class: bitcoin_script
|
||||
tier: yield_enabled
|
||||
|
||||
signer:
|
||||
protocol: cggmp21
|
||||
curve: secp256k1
|
||||
threshold:
|
||||
t: 2
|
||||
n: 3
|
||||
native_signer:
|
||||
protocol: frost
|
||||
curve: secp256k1 # Schnorr over secp256k1 for Taproot
|
||||
|
||||
finality:
|
||||
mode: block_depth
|
||||
confirmations: 6
|
||||
max_reorg_depth: 100
|
||||
stale_attestation_seconds: 86400
|
||||
|
||||
assets:
|
||||
- ticker: BTC
|
||||
decimals: 8
|
||||
recipient_encoding: bech32
|
||||
wrapper:
|
||||
symbol: LBTC
|
||||
contract: "liquid/tokens/LBTC.sol"
|
||||
decimals: 18 # LBTC is 18 decimals on Lux C-Chain (scaled from 8)
|
||||
strategy:
|
||||
name: BabylonBTCStrategy
|
||||
contract: "bridge/teleport/strategies/BabylonBTCStrategy.sol"
|
||||
|
||||
adapter:
|
||||
deposit_method: utxo_watch
|
||||
vault_contract: "kms://teleport/opnet/taproot-address"
|
||||
teleporter_contract: "kms://teleport/lux-c/teleporter-btc"
|
||||
|
||||
watcher:
|
||||
plugin: bitcoin_zmq_watcher
|
||||
rpc_url: "kms://teleport/opnet/bitcoin-rpc"
|
||||
poll_interval_ms: 60000 # ~1 Bitcoin block
|
||||
start_block: 0
|
||||
@@ -0,0 +1,48 @@
|
||||
# Optimism — Teleport Chain Manifest
|
||||
# Schema: ../schema.yaml v1.0.0
|
||||
|
||||
chain_id: 10
|
||||
name: "Optimism"
|
||||
class: evm
|
||||
tier: bidirectional
|
||||
|
||||
signer:
|
||||
protocol: cggmp21
|
||||
curve: secp256k1
|
||||
threshold:
|
||||
t: 2
|
||||
n: 3
|
||||
|
||||
finality:
|
||||
mode: block_depth
|
||||
confirmations: 64 # L2 blocks, ~2s; wait for L1 batch posting
|
||||
max_reorg_depth: 128
|
||||
stale_attestation_seconds: 86400
|
||||
|
||||
assets:
|
||||
- ticker: ETH
|
||||
decimals: 18
|
||||
recipient_encoding: evm_address
|
||||
wrapper:
|
||||
symbol: LETH
|
||||
contract: "liquid/tokens/LETH.sol"
|
||||
decimals: 18
|
||||
|
||||
- ticker: OP
|
||||
decimals: 18
|
||||
recipient_encoding: evm_address
|
||||
wrapper:
|
||||
symbol: LOP
|
||||
contract: "liquid/tokens/LOP.sol"
|
||||
decimals: 18
|
||||
|
||||
adapter:
|
||||
deposit_method: event_log
|
||||
vault_contract: "kms://teleport/optimism/teleport-vault"
|
||||
teleporter_contract: "kms://teleport/lux-c/teleporter-op"
|
||||
|
||||
watcher:
|
||||
plugin: evm_log_watcher
|
||||
rpc_url: "kms://teleport/optimism/rpc"
|
||||
poll_interval_ms: 2000
|
||||
start_block: 0
|
||||
@@ -0,0 +1,42 @@
|
||||
# Polkadot — Teleport Chain Manifest
|
||||
# Schema: ../schema.yaml v1.0.0
|
||||
#
|
||||
# Status: locale_only — FROST sr25519 signer pending (Schnorr over Ristretto255).
|
||||
|
||||
chain_id: 4294967380
|
||||
name: "Polkadot"
|
||||
class: account_native
|
||||
tier: locale_only
|
||||
|
||||
signer:
|
||||
protocol: frost
|
||||
curve: ed25519 # sr25519 (Schnorrkel) uses Ristretto255; mapped to ed25519 in schema
|
||||
threshold:
|
||||
t: 2
|
||||
n: 3
|
||||
|
||||
finality:
|
||||
mode: slot_finalized # GRANDPA finality gadget
|
||||
confirmations: 2
|
||||
max_reorg_depth: 8
|
||||
stale_attestation_seconds: 86400
|
||||
|
||||
assets:
|
||||
- ticker: DOT
|
||||
decimals: 10
|
||||
recipient_encoding: raw_bytes # SS58-encoded Substrate address
|
||||
wrapper:
|
||||
symbol: LDOT
|
||||
contract: "liquid/tokens/LDOT.sol"
|
||||
decimals: 18
|
||||
|
||||
adapter:
|
||||
deposit_method: account_watch
|
||||
vault_contract: "kms://teleport/polkadot/mpc-address"
|
||||
teleporter_contract: "kms://teleport/lux-c/teleporter-dot"
|
||||
|
||||
watcher:
|
||||
plugin: substrate_ws_watcher
|
||||
rpc_url: "kms://teleport/polkadot/rpc"
|
||||
poll_interval_ms: 6000
|
||||
start_block: 0
|
||||
@@ -0,0 +1,48 @@
|
||||
# Polygon PoS — Teleport Chain Manifest
|
||||
# Schema: ../schema.yaml v1.0.0
|
||||
|
||||
chain_id: 137
|
||||
name: "Polygon"
|
||||
class: evm
|
||||
tier: bidirectional
|
||||
|
||||
signer:
|
||||
protocol: cggmp21
|
||||
curve: secp256k1
|
||||
threshold:
|
||||
t: 2
|
||||
n: 3
|
||||
|
||||
finality:
|
||||
mode: block_depth
|
||||
confirmations: 128 # Polygon PoS requires deep confirmation for safety
|
||||
max_reorg_depth: 256
|
||||
stale_attestation_seconds: 86400
|
||||
|
||||
assets:
|
||||
- ticker: POL
|
||||
decimals: 18
|
||||
recipient_encoding: evm_address
|
||||
wrapper:
|
||||
symbol: LPOL
|
||||
contract: "liquid/tokens/LPOL.sol"
|
||||
decimals: 18
|
||||
|
||||
- ticker: USDC
|
||||
decimals: 6
|
||||
recipient_encoding: evm_address
|
||||
wrapper:
|
||||
symbol: LUSD
|
||||
contract: "liquid/tokens/LUSD.sol"
|
||||
decimals: 18
|
||||
|
||||
adapter:
|
||||
deposit_method: event_log
|
||||
vault_contract: "kms://teleport/polygon/teleport-vault"
|
||||
teleporter_contract: "kms://teleport/lux-c/teleporter-pol"
|
||||
|
||||
watcher:
|
||||
plugin: evm_log_watcher
|
||||
rpc_url: "kms://teleport/polygon/rpc"
|
||||
poll_interval_ms: 2000
|
||||
start_block: 0
|
||||
@@ -0,0 +1,43 @@
|
||||
# Solana — Teleport Chain Manifest
|
||||
# Schema: ../schema.yaml v1.0.0
|
||||
#
|
||||
# Status: locale_only — FROST Ed25519 signer integration pending.
|
||||
# Once signer is ready, promote to inbound_only, then bidirectional.
|
||||
|
||||
chain_id: 1399811149 # 0x536F6C4D — "SolM" in ASCII
|
||||
name: "Solana"
|
||||
class: account_native
|
||||
tier: locale_only
|
||||
|
||||
signer:
|
||||
protocol: frost
|
||||
curve: ed25519
|
||||
threshold:
|
||||
t: 2
|
||||
n: 3
|
||||
|
||||
finality:
|
||||
mode: slot_finalized
|
||||
confirmations: 32 # ~32 slots for safe finality
|
||||
max_reorg_depth: 64
|
||||
stale_attestation_seconds: 86400
|
||||
|
||||
assets:
|
||||
- ticker: SOL
|
||||
decimals: 9
|
||||
recipient_encoding: base58
|
||||
wrapper:
|
||||
symbol: LSOL
|
||||
contract: "liquid/tokens/LSOL.sol"
|
||||
decimals: 18
|
||||
|
||||
adapter:
|
||||
deposit_method: account_watch
|
||||
vault_contract: "kms://teleport/solana/mpc-program"
|
||||
teleporter_contract: "kms://teleport/lux-c/teleporter-sol"
|
||||
|
||||
watcher:
|
||||
plugin: solana_ws_watcher
|
||||
rpc_url: "kms://teleport/solana/rpc"
|
||||
poll_interval_ms: 400 # ~400ms Solana slot time
|
||||
start_block: 0
|
||||
@@ -0,0 +1,46 @@
|
||||
# Stacks — Teleport Chain Manifest
|
||||
# Schema: ../schema.yaml v1.0.0
|
||||
#
|
||||
# Status: locale_only — Clarity/Bitcoin script adapter pending.
|
||||
# Stacks settles on Bitcoin L1 with Clarity smart contracts.
|
||||
|
||||
chain_id: 4294967440 # STACKS_MAINNET from ChainIds.sol
|
||||
name: "Stacks"
|
||||
class: bitcoin_script
|
||||
tier: locale_only
|
||||
|
||||
signer:
|
||||
protocol: cggmp21
|
||||
curve: secp256k1
|
||||
threshold:
|
||||
t: 2
|
||||
n: 3
|
||||
native_signer:
|
||||
protocol: frost
|
||||
curve: secp256k1 # Schnorr for Stacks Nakamoto upgrade
|
||||
|
||||
finality:
|
||||
mode: block_depth
|
||||
confirmations: 6 # Anchored to Bitcoin — 6 Bitcoin block confirmations
|
||||
max_reorg_depth: 100
|
||||
stale_attestation_seconds: 86400
|
||||
|
||||
assets:
|
||||
- ticker: STX
|
||||
decimals: 6
|
||||
recipient_encoding: base58 # Stacks SP.../SM... c32check addresses
|
||||
wrapper:
|
||||
symbol: LSTX
|
||||
contract: "liquid/tokens/LSTX.sol"
|
||||
decimals: 18
|
||||
|
||||
adapter:
|
||||
deposit_method: utxo_watch
|
||||
vault_contract: "kms://teleport/stacks/mpc-address"
|
||||
teleporter_contract: "kms://teleport/lux-c/teleporter-stx"
|
||||
|
||||
watcher:
|
||||
plugin: stacks_api_watcher
|
||||
rpc_url: "kms://teleport/stacks/rpc"
|
||||
poll_interval_ms: 30000
|
||||
start_block: 0
|
||||
@@ -0,0 +1,43 @@
|
||||
# StarkNet — Teleport Chain Manifest
|
||||
# Schema: ../schema.yaml v1.0.0
|
||||
#
|
||||
# Status: locale_only — FROST stark_curve signer pending.
|
||||
# StarkNet uses STARK-friendly curve (Pedersen hash, ECDSA over stark curve).
|
||||
|
||||
chain_id: 23448594291968334 # SN_MAIN from ChainIds.sol
|
||||
name: "StarkNet"
|
||||
class: account_native
|
||||
tier: locale_only
|
||||
|
||||
signer:
|
||||
protocol: frost
|
||||
curve: ed25519 # stark_curve mapped to ed25519 in schema; actual is Stark curve
|
||||
threshold:
|
||||
t: 2
|
||||
n: 3
|
||||
|
||||
finality:
|
||||
mode: slot_finalized # L1 proof acceptance
|
||||
confirmations: 1
|
||||
max_reorg_depth: 0
|
||||
stale_attestation_seconds: 86400
|
||||
|
||||
assets:
|
||||
- ticker: ETH
|
||||
decimals: 18
|
||||
recipient_encoding: raw_bytes # 32-byte felt StarkNet address
|
||||
wrapper:
|
||||
symbol: LETH
|
||||
contract: "liquid/tokens/LETH.sol"
|
||||
decimals: 18
|
||||
|
||||
adapter:
|
||||
deposit_method: account_watch
|
||||
vault_contract: "kms://teleport/starknet/mpc-address"
|
||||
teleporter_contract: "kms://teleport/lux-c/teleporter-starknet"
|
||||
|
||||
watcher:
|
||||
plugin: starknet_ws_watcher
|
||||
rpc_url: "kms://teleport/starknet/rpc"
|
||||
poll_interval_ms: 30000
|
||||
start_block: 0
|
||||
@@ -0,0 +1,42 @@
|
||||
# Stellar — Teleport Chain Manifest
|
||||
# Schema: ../schema.yaml v1.0.0
|
||||
#
|
||||
# Status: locale_only — FROST Ed25519 signer integration pending.
|
||||
|
||||
chain_id: 4294967410
|
||||
name: "Stellar"
|
||||
class: account_native
|
||||
tier: locale_only
|
||||
|
||||
signer:
|
||||
protocol: frost
|
||||
curve: ed25519
|
||||
threshold:
|
||||
t: 2
|
||||
n: 3
|
||||
|
||||
finality:
|
||||
mode: slot_finalized # Stellar Consensus Protocol — immediate finality
|
||||
confirmations: 1
|
||||
max_reorg_depth: 0
|
||||
stale_attestation_seconds: 86400
|
||||
|
||||
assets:
|
||||
- ticker: XLM
|
||||
decimals: 7
|
||||
recipient_encoding: raw_bytes # G... base32 Stellar public key
|
||||
wrapper:
|
||||
symbol: LXLM
|
||||
contract: "liquid/tokens/LXLM.sol"
|
||||
decimals: 18
|
||||
|
||||
adapter:
|
||||
deposit_method: account_watch
|
||||
vault_contract: "kms://teleport/stellar/mpc-address"
|
||||
teleporter_contract: "kms://teleport/lux-c/teleporter-xlm"
|
||||
|
||||
watcher:
|
||||
plugin: stellar_horizon_watcher
|
||||
rpc_url: "kms://teleport/stellar/horizon"
|
||||
poll_interval_ms: 5000
|
||||
start_block: 0
|
||||
@@ -0,0 +1,42 @@
|
||||
# Sui — Teleport Chain Manifest
|
||||
# Schema: ../schema.yaml v1.0.0
|
||||
#
|
||||
# Status: locale_only — FROST Ed25519 signer integration pending.
|
||||
|
||||
chain_id: 4294967330
|
||||
name: "Sui"
|
||||
class: account_native
|
||||
tier: locale_only
|
||||
|
||||
signer:
|
||||
protocol: frost
|
||||
curve: ed25519
|
||||
threshold:
|
||||
t: 2
|
||||
n: 3
|
||||
|
||||
finality:
|
||||
mode: slot_finalized # Sui checkpoint-based finality
|
||||
confirmations: 1
|
||||
max_reorg_depth: 0
|
||||
stale_attestation_seconds: 86400
|
||||
|
||||
assets:
|
||||
- ticker: SUI
|
||||
decimals: 9
|
||||
recipient_encoding: raw_bytes # 32-byte Sui address
|
||||
wrapper:
|
||||
symbol: LSUI
|
||||
contract: "liquid/tokens/LSUI.sol"
|
||||
decimals: 18
|
||||
|
||||
adapter:
|
||||
deposit_method: account_watch
|
||||
vault_contract: "kms://teleport/sui/mpc-address"
|
||||
teleporter_contract: "kms://teleport/lux-c/teleporter-sui"
|
||||
|
||||
watcher:
|
||||
plugin: sui_ws_watcher
|
||||
rpc_url: "kms://teleport/sui/rpc"
|
||||
poll_interval_ms: 3000
|
||||
start_block: 0
|
||||
@@ -0,0 +1,42 @@
|
||||
# Tezos — Teleport Chain Manifest
|
||||
# Schema: ../schema.yaml v1.0.0
|
||||
#
|
||||
# Status: locale_only — adapter integration pending.
|
||||
|
||||
chain_id: 4294967500 # TEZOS_MAINNET from ChainIds.sol
|
||||
name: "Tezos"
|
||||
class: account_native
|
||||
tier: locale_only
|
||||
|
||||
signer:
|
||||
protocol: frost
|
||||
curve: ed25519
|
||||
threshold:
|
||||
t: 2
|
||||
n: 3
|
||||
|
||||
finality:
|
||||
mode: slot_finalized # Tenderbake BFT finality (~30s)
|
||||
confirmations: 2
|
||||
max_reorg_depth: 0
|
||||
stale_attestation_seconds: 86400
|
||||
|
||||
assets:
|
||||
- ticker: XTZ
|
||||
decimals: 6
|
||||
recipient_encoding: raw_bytes # tz1... base58check Tezos addresses
|
||||
wrapper:
|
||||
symbol: LXTZ
|
||||
contract: "liquid/tokens/LXTZ.sol"
|
||||
decimals: 18
|
||||
|
||||
adapter:
|
||||
deposit_method: account_watch
|
||||
vault_contract: "kms://teleport/tezos/mpc-address"
|
||||
teleporter_contract: "kms://teleport/lux-c/teleporter-xtz"
|
||||
|
||||
watcher:
|
||||
plugin: tezos_ws_watcher
|
||||
rpc_url: "kms://teleport/tezos/rpc"
|
||||
poll_interval_ms: 15000
|
||||
start_block: 0
|
||||
@@ -0,0 +1,42 @@
|
||||
# TON (The Open Network) — Teleport Chain Manifest
|
||||
# Schema: ../schema.yaml v1.0.0
|
||||
#
|
||||
# Status: locale_only — FROST Ed25519 signer integration pending.
|
||||
|
||||
chain_id: 1414483791 # 0x544F4E4F — "TONO" in ASCII
|
||||
name: "TON"
|
||||
class: account_native
|
||||
tier: locale_only
|
||||
|
||||
signer:
|
||||
protocol: frost
|
||||
curve: ed25519
|
||||
threshold:
|
||||
t: 2
|
||||
n: 3
|
||||
|
||||
finality:
|
||||
mode: block_depth
|
||||
confirmations: 1 # TON has fast finality via catchain
|
||||
max_reorg_depth: 8
|
||||
stale_attestation_seconds: 86400
|
||||
|
||||
assets:
|
||||
- ticker: TON
|
||||
decimals: 9
|
||||
recipient_encoding: raw_bytes # 36-byte TON address
|
||||
wrapper:
|
||||
symbol: LTON
|
||||
contract: "liquid/tokens/LTON.sol"
|
||||
decimals: 18
|
||||
|
||||
adapter:
|
||||
deposit_method: account_watch
|
||||
vault_contract: "kms://teleport/ton/mpc-wallet"
|
||||
teleporter_contract: "kms://teleport/lux-c/teleporter-ton"
|
||||
|
||||
watcher:
|
||||
plugin: ton_liteserver_watcher
|
||||
rpc_url: "kms://teleport/ton/liteserver"
|
||||
poll_interval_ms: 5000
|
||||
start_block: 0
|
||||
@@ -0,0 +1,43 @@
|
||||
# TRON — Teleport Chain Manifest
|
||||
# Schema: ../schema.yaml v1.0.0
|
||||
#
|
||||
# Status: locale_only — TVM adapter integration pending.
|
||||
# TRON uses TVM (near-EVM Solidity-compatible) with secp256k1 signing.
|
||||
|
||||
chain_id: 728126
|
||||
name: "TRON"
|
||||
class: evm
|
||||
tier: locale_only
|
||||
|
||||
signer:
|
||||
protocol: cggmp21
|
||||
curve: secp256k1
|
||||
threshold:
|
||||
t: 2
|
||||
n: 3
|
||||
|
||||
finality:
|
||||
mode: block_depth
|
||||
confirmations: 19 # 19 SRs confirm — ~57s
|
||||
max_reorg_depth: 32
|
||||
stale_attestation_seconds: 86400
|
||||
|
||||
assets:
|
||||
- ticker: TRX
|
||||
decimals: 6
|
||||
recipient_encoding: base58 # T... base58check TRON addresses
|
||||
wrapper:
|
||||
symbol: LTRX
|
||||
contract: "liquid/tokens/LTRX.sol"
|
||||
decimals: 18
|
||||
|
||||
adapter:
|
||||
deposit_method: event_log
|
||||
vault_contract: "kms://teleport/tron/teleport-vault"
|
||||
teleporter_contract: "kms://teleport/lux-c/teleporter-trx"
|
||||
|
||||
watcher:
|
||||
plugin: evm_log_watcher
|
||||
rpc_url: "kms://teleport/tron/rpc"
|
||||
poll_interval_ms: 3000
|
||||
start_block: 0
|
||||
@@ -0,0 +1,42 @@
|
||||
# XRP Ledger — Teleport Chain Manifest
|
||||
# Schema: ../schema.yaml v1.0.0
|
||||
#
|
||||
# Status: locale_only — CGGMP21 secp256k1 adapter integration pending.
|
||||
|
||||
chain_id: 4294967420
|
||||
name: "XRPL"
|
||||
class: account_native
|
||||
tier: locale_only
|
||||
|
||||
signer:
|
||||
protocol: cggmp21
|
||||
curve: secp256k1
|
||||
threshold:
|
||||
t: 2
|
||||
n: 3
|
||||
|
||||
finality:
|
||||
mode: slot_finalized # XRPL consensus — ledger validated in ~4s
|
||||
confirmations: 1
|
||||
max_reorg_depth: 0
|
||||
stale_attestation_seconds: 86400
|
||||
|
||||
assets:
|
||||
- ticker: XRP
|
||||
decimals: 6
|
||||
recipient_encoding: base58 # r... base58check XRPL addresses
|
||||
wrapper:
|
||||
symbol: LXRP
|
||||
contract: "liquid/tokens/LXRP.sol"
|
||||
decimals: 18
|
||||
|
||||
adapter:
|
||||
deposit_method: account_watch
|
||||
vault_contract: "kms://teleport/xrpl/mpc-address"
|
||||
teleporter_contract: "kms://teleport/lux-c/teleporter-xrp"
|
||||
|
||||
watcher:
|
||||
plugin: xrpl_ws_watcher
|
||||
rpc_url: "kms://teleport/xrpl/rpc"
|
||||
poll_interval_ms: 4000
|
||||
start_block: 0
|
||||
@@ -0,0 +1,259 @@
|
||||
# Teleport Chain Manifest Schema
|
||||
# Every chain integration in the Lux Teleport bridge MUST provide a manifest
|
||||
# conforming to this schema. The manifest is the single source of truth for:
|
||||
# 1. Locale registration in TeleportProposalBridge
|
||||
# 2. Source-chain adapter semantics (deposit/release)
|
||||
# 3. Watcher/finality plugin configuration
|
||||
# 4. MPC signer key type
|
||||
# 5. Asset definitions (ticker, decimals, recipient encoding, canonical wrapper)
|
||||
# 6. Conformance test pack reference
|
||||
#
|
||||
# Validate with: yq eval 'true' manifests/chains/<chain>.yaml
|
||||
|
||||
schema_version: "1.0.0"
|
||||
|
||||
# ============================================================================
|
||||
# REQUIRED FIELDS
|
||||
# ============================================================================
|
||||
|
||||
# chain_id: uint64 — canonical chain identifier.
|
||||
# EVM chains use their native chainId.
|
||||
# Non-EVM chains use a deterministic encoding (e.g. OP_NET = 0x4F504E45).
|
||||
# Lux C-Chain = 96369.
|
||||
chain_id:
|
||||
type: integer
|
||||
required: true
|
||||
min: 1
|
||||
|
||||
# name: human-readable chain name for logs/UIs
|
||||
name:
|
||||
type: string
|
||||
required: true
|
||||
|
||||
# class: chain family — determines adapter and signer selection
|
||||
# lux_native — Lux subnets (C-Chain, D-Chain). Warp messaging, BLS finality.
|
||||
# evm — EVM-compatible L1/L2. ECDSA signer, block finality.
|
||||
# bitcoin_script — Bitcoin-family UTXO chains. Taproot/P2WSH custody, block finality.
|
||||
# account_native — Non-EVM account-model chains (Solana, TON, Sui). Chain-specific signer.
|
||||
class:
|
||||
type: string
|
||||
required: true
|
||||
enum: [lux_native, evm, bitcoin_script, account_native]
|
||||
|
||||
# tier: integration depth — determines which Teleport flows are enabled
|
||||
# locale_only — Registered as locale; no active bridge (pending signer/adapter).
|
||||
# inbound_only — Deposits accepted, minting on Lux. No withdrawals yet.
|
||||
# bidirectional — Full deposit + withdraw. MPC custody live.
|
||||
# yield_enabled — Bidirectional + yield strategy routing via LiquidVault.
|
||||
tier:
|
||||
type: string
|
||||
required: true
|
||||
enum: [locale_only, inbound_only, bidirectional, yield_enabled]
|
||||
|
||||
# ============================================================================
|
||||
# SIGNER CONFIGURATION
|
||||
# ============================================================================
|
||||
|
||||
signer:
|
||||
type: object
|
||||
required: true
|
||||
properties:
|
||||
# protocol: MPC signing protocol used for this chain's key type
|
||||
# cggmp21 — ECDSA threshold (secp256k1). EVM and Bitcoin.
|
||||
# frost — Schnorr/EdDSA threshold. Solana, TON, Lux validators.
|
||||
# bls — BLS aggregation. Lux Warp messaging.
|
||||
# corona — Post-quantum lattice threshold. Future upgrade path.
|
||||
protocol:
|
||||
type: string
|
||||
required: true
|
||||
enum: [cggmp21, frost, bls, corona]
|
||||
|
||||
# curve: underlying elliptic curve
|
||||
curve:
|
||||
type: string
|
||||
required: true
|
||||
enum: [secp256k1, ed25519, bls12_381, ml_dsa_65]
|
||||
|
||||
# threshold: t-of-n signing quorum
|
||||
threshold:
|
||||
type: object
|
||||
required: true
|
||||
properties:
|
||||
t:
|
||||
type: integer
|
||||
min: 2
|
||||
n:
|
||||
type: integer
|
||||
min: 3
|
||||
|
||||
# native_signer: optional secondary signer for chain-native operations
|
||||
# (e.g. FROST for Taproot on Bitcoin, BLS for Warp on Lux)
|
||||
native_signer:
|
||||
type: object
|
||||
required: false
|
||||
properties:
|
||||
protocol:
|
||||
type: string
|
||||
enum: [frost, bls, corona]
|
||||
curve:
|
||||
type: string
|
||||
enum: [secp256k1, ed25519, bls12_381]
|
||||
|
||||
# ============================================================================
|
||||
# FINALITY RULES
|
||||
# ============================================================================
|
||||
|
||||
finality:
|
||||
type: object
|
||||
required: true
|
||||
properties:
|
||||
# mode: how finality is determined
|
||||
# block_depth — Wait N confirmations.
|
||||
# slot_finalized — Wait for slot finalization (Solana, Ethereum post-merge).
|
||||
# instant — Single-slot finality (Lux Snowman).
|
||||
mode:
|
||||
type: string
|
||||
required: true
|
||||
enum: [block_depth, slot_finalized, instant]
|
||||
|
||||
# confirmations: number of blocks/slots to wait (block_depth mode)
|
||||
confirmations:
|
||||
type: integer
|
||||
required: false
|
||||
min: 1
|
||||
|
||||
# max_reorg_depth: deepest reorg the watcher should tolerate before halting
|
||||
max_reorg_depth:
|
||||
type: integer
|
||||
required: false
|
||||
|
||||
# stale_attestation_seconds: max age of a backing attestation before Teleporter
|
||||
# rejects minting (maps to Teleporter._checkBackingRatio 24h default)
|
||||
stale_attestation_seconds:
|
||||
type: integer
|
||||
required: true
|
||||
default: 86400 # 24 hours
|
||||
|
||||
# ============================================================================
|
||||
# ASSETS
|
||||
# ============================================================================
|
||||
|
||||
assets:
|
||||
type: array
|
||||
required: true
|
||||
min_items: 1
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
# ticker: canonical asset symbol on the source chain
|
||||
ticker:
|
||||
type: string
|
||||
required: true
|
||||
|
||||
# decimals: native decimal precision on source chain
|
||||
decimals:
|
||||
type: integer
|
||||
required: true
|
||||
|
||||
# recipient_encoding: how recipient addresses are serialized
|
||||
# evm_address — 20-byte hex (0x...)
|
||||
# bech32 — Bitcoin bech32/bech32m (bc1...)
|
||||
# base58 — Solana/Bitcoin legacy
|
||||
# raw_bytes — 32-byte or 36-byte (TON, Sui)
|
||||
recipient_encoding:
|
||||
type: string
|
||||
required: true
|
||||
enum: [evm_address, bech32, base58, raw_bytes]
|
||||
|
||||
# wrapper: Liquid token minted on Lux C-Chain for this asset
|
||||
wrapper:
|
||||
type: object
|
||||
required: true
|
||||
properties:
|
||||
symbol:
|
||||
type: string
|
||||
required: true
|
||||
contract:
|
||||
type: string
|
||||
required: true
|
||||
description: "Path relative to @luxfi/standard/contracts/"
|
||||
decimals:
|
||||
type: integer
|
||||
required: true
|
||||
|
||||
# strategy: yield strategy (only for tier=yield_enabled)
|
||||
strategy:
|
||||
type: object
|
||||
required: false
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
contract:
|
||||
type: string
|
||||
description: "Path relative to @luxfi/standard/contracts/"
|
||||
|
||||
# ============================================================================
|
||||
# ADAPTER CONFIGURATION
|
||||
# ============================================================================
|
||||
|
||||
adapter:
|
||||
type: object
|
||||
required: true
|
||||
properties:
|
||||
# deposit_method: how deposits are detected on the source chain
|
||||
# event_log — EVM event (TeleportVault.Deposit)
|
||||
# utxo_watch — Monitor UTXO to MPC-controlled address
|
||||
# account_watch — Monitor balance/instruction on account-model chain
|
||||
deposit_method:
|
||||
type: string
|
||||
required: true
|
||||
enum: [event_log, utxo_watch, account_watch]
|
||||
|
||||
# vault_contract: source-chain vault address or MPC custody address
|
||||
# For EVM: deployed TeleportVault/LiquidVault contract address
|
||||
# For Bitcoin: MPC-controlled Taproot address
|
||||
# For account-native: MPC-controlled program/account address
|
||||
vault_contract:
|
||||
type: string
|
||||
required: true
|
||||
|
||||
# teleporter_contract: Lux C-Chain Teleporter contract that mints L* tokens
|
||||
teleporter_contract:
|
||||
type: string
|
||||
required: true
|
||||
|
||||
# ============================================================================
|
||||
# WATCHER CONFIGURATION
|
||||
# ============================================================================
|
||||
|
||||
watcher:
|
||||
type: object
|
||||
required: true
|
||||
properties:
|
||||
# plugin: watcher implementation to use
|
||||
# evm_log_watcher — Subscribe to EVM logs via eth_subscribe/getLogs
|
||||
# bitcoin_zmq_watcher — Subscribe to Bitcoin ZMQ block notifications
|
||||
# solana_ws_watcher — Subscribe to Solana WebSocket account changes
|
||||
# ton_liteserver_watcher — Poll TON liteserver for transactions
|
||||
# lux_warp_watcher — Subscribe to Lux Warp messages
|
||||
plugin:
|
||||
type: string
|
||||
required: true
|
||||
|
||||
# rpc_url: RPC endpoint (resolved from KMS at runtime, placeholder here)
|
||||
rpc_url:
|
||||
type: string
|
||||
required: true
|
||||
description: "KMS secret path or well-known endpoint"
|
||||
|
||||
# poll_interval_ms: polling interval for non-streaming watchers
|
||||
poll_interval_ms:
|
||||
type: integer
|
||||
required: false
|
||||
default: 12000
|
||||
|
||||
# start_block: block/slot to start watching from (0 = latest)
|
||||
start_block:
|
||||
type: integer
|
||||
required: false
|
||||
default: 0
|
||||
@@ -0,0 +1,34 @@
|
||||
# Hanzo Network — Tenant Manifest
|
||||
# Schema: schema.yaml v1.0.0
|
||||
#
|
||||
# Hanzo AI bridge. EVM chains + BTC.
|
||||
|
||||
id: hanzo
|
||||
name: "Hanzo Network"
|
||||
|
||||
hostnames:
|
||||
- bridge.hanzo.ai
|
||||
|
||||
home_chain_id: 36963 # Hanzo C-Chain
|
||||
|
||||
supported_chains:
|
||||
- lux-c
|
||||
- ethereum
|
||||
- base
|
||||
- opnet
|
||||
|
||||
teleporter_addresses:
|
||||
"96369": "kms://teleport/lux-c/teleporter"
|
||||
"1": "kms://teleport/lux-c/teleporter-eth"
|
||||
"8453": "kms://teleport/lux-c/teleporter-eth"
|
||||
"4294967299": "kms://teleport/lux-c/teleporter-btc"
|
||||
|
||||
branding:
|
||||
logo_url: "/static/hanzo/logo.svg"
|
||||
primary_color: "#6366F1"
|
||||
theme: dark
|
||||
|
||||
features:
|
||||
swap: true
|
||||
yield: true
|
||||
perps: true
|
||||
@@ -0,0 +1,39 @@
|
||||
# Lux Network — Tenant Manifest
|
||||
# Schema: schema.yaml v1.0.0
|
||||
#
|
||||
# Primary bridge deployment. All chains supported. Full feature set.
|
||||
|
||||
id: lux
|
||||
name: "Lux Network"
|
||||
|
||||
hostnames:
|
||||
- bridge.lux.network
|
||||
- bridge.lux.io
|
||||
|
||||
home_chain_id: 96369 # Lux C-Chain
|
||||
|
||||
supported_chains:
|
||||
- lux-c
|
||||
- ethereum
|
||||
- base
|
||||
- opnet
|
||||
- solana
|
||||
- ton
|
||||
|
||||
teleporter_addresses:
|
||||
"96369": "kms://teleport/lux-c/teleporter"
|
||||
"1": "kms://teleport/lux-c/teleporter-eth"
|
||||
"8453": "kms://teleport/lux-c/teleporter-eth"
|
||||
"4294967299": "kms://teleport/lux-c/teleporter-btc"
|
||||
"1399811149": "kms://teleport/lux-c/teleporter-sol"
|
||||
"1414483791": "kms://teleport/lux-c/teleporter-ton"
|
||||
|
||||
branding:
|
||||
logo_url: "/static/lux/logo.svg"
|
||||
primary_color: "#E84142"
|
||||
theme: dark
|
||||
|
||||
features:
|
||||
swap: true
|
||||
yield: true
|
||||
perps: true
|
||||
@@ -0,0 +1,32 @@
|
||||
# Pars Network — Tenant Manifest
|
||||
# Schema: schema.yaml v1.0.0
|
||||
#
|
||||
# Pars bridge. EVM chains focus.
|
||||
|
||||
id: pars
|
||||
name: "Pars Network"
|
||||
|
||||
hostnames:
|
||||
- bridge.pars.network
|
||||
|
||||
home_chain_id: 494949 # Pars C-Chain
|
||||
|
||||
supported_chains:
|
||||
- lux-c
|
||||
- ethereum
|
||||
- base
|
||||
|
||||
teleporter_addresses:
|
||||
"96369": "kms://teleport/lux-c/teleporter"
|
||||
"1": "kms://teleport/lux-c/teleporter-eth"
|
||||
"8453": "kms://teleport/lux-c/teleporter-eth"
|
||||
|
||||
branding:
|
||||
logo_url: "/static/pars/logo.svg"
|
||||
primary_color: "#F59E0B"
|
||||
theme: light
|
||||
|
||||
features:
|
||||
swap: true
|
||||
yield: false
|
||||
perps: false
|
||||
@@ -0,0 +1,119 @@
|
||||
# Tenant Manifest Schema
|
||||
# Each white-label bridge deployment MUST provide a tenant manifest conforming
|
||||
# to this schema. The manifest is the single source of truth for:
|
||||
# 1. Hostname-based routing (which domain -> which tenant)
|
||||
# 2. Supported chain set (references chain manifest files)
|
||||
# 3. Per-chain Teleporter contract addresses
|
||||
# 4. Frontend branding configuration
|
||||
# 5. Feature flags
|
||||
#
|
||||
# Validate with: yq eval 'true' manifests/tenants/<tenant>.yaml
|
||||
|
||||
schema_version: "1.0.0"
|
||||
|
||||
# ============================================================================
|
||||
# REQUIRED FIELDS
|
||||
# ============================================================================
|
||||
|
||||
# id: unique tenant slug used as internal identifier and ConfigMap key.
|
||||
# Must be lowercase alphanumeric + hyphens. e.g., "lux", "zoo", "hanzo"
|
||||
id:
|
||||
type: string
|
||||
required: true
|
||||
pattern: "^[a-z0-9][a-z0-9-]*$"
|
||||
|
||||
# name: human-readable display name for the bridge UI.
|
||||
name:
|
||||
type: string
|
||||
required: true
|
||||
|
||||
# hostnames: list of DNS hostnames that route to this tenant.
|
||||
# The opnet-watcher API resolves Host header against this list.
|
||||
# First entry is canonical; others are aliases.
|
||||
hostnames:
|
||||
type: array
|
||||
required: true
|
||||
min_items: 1
|
||||
items:
|
||||
type: string
|
||||
|
||||
# home_chain_id: primary chain for this tenant.
|
||||
# The bridge UI defaults to this chain as the "home" network.
|
||||
# Must reference a chain_id from a chain manifest in supported_chains.
|
||||
home_chain_id:
|
||||
type: integer
|
||||
required: true
|
||||
min: 1
|
||||
|
||||
# supported_chains: list of chain manifest filenames (without .yaml extension).
|
||||
# Each entry must correspond to a file in manifests/chains/<name>.yaml.
|
||||
# Only these chains are available for bridging under this tenant.
|
||||
supported_chains:
|
||||
type: array
|
||||
required: true
|
||||
min_items: 1
|
||||
items:
|
||||
type: string
|
||||
|
||||
# ============================================================================
|
||||
# TELEPORTER ADDRESSES
|
||||
# ============================================================================
|
||||
|
||||
# teleporter_addresses: per-chain Teleporter contract addresses for this tenant.
|
||||
# Keys are chain_id (as string), values are 0x-prefixed contract addresses
|
||||
# or KMS secret paths (kms://...).
|
||||
# Only chains listed in supported_chains need entries here.
|
||||
teleporter_addresses:
|
||||
type: object
|
||||
required: true
|
||||
additionalProperties:
|
||||
type: string
|
||||
description: "0x-prefixed address or kms:// secret path"
|
||||
|
||||
# ============================================================================
|
||||
# BRANDING
|
||||
# ============================================================================
|
||||
|
||||
branding:
|
||||
type: object
|
||||
required: true
|
||||
properties:
|
||||
# logo_url: path or URL to the tenant logo (served by hanzoai/static)
|
||||
logo_url:
|
||||
type: string
|
||||
required: true
|
||||
|
||||
# primary_color: hex color for UI accent elements
|
||||
primary_color:
|
||||
type: string
|
||||
required: true
|
||||
pattern: "^#[0-9a-fA-F]{6}$"
|
||||
|
||||
# theme: light or dark base theme
|
||||
theme:
|
||||
type: string
|
||||
required: true
|
||||
enum: [light, dark]
|
||||
|
||||
# ============================================================================
|
||||
# FEATURE FLAGS
|
||||
# ============================================================================
|
||||
|
||||
features:
|
||||
type: object
|
||||
required: true
|
||||
properties:
|
||||
# swap: enable cross-chain swap UI
|
||||
swap:
|
||||
type: boolean
|
||||
required: true
|
||||
|
||||
# yield: enable yield strategy display and routing
|
||||
yield:
|
||||
type: boolean
|
||||
required: true
|
||||
|
||||
# perps: enable perpetuals trading UI
|
||||
perps:
|
||||
type: boolean
|
||||
required: true
|
||||
@@ -0,0 +1,32 @@
|
||||
# SPC Network — Tenant Manifest
|
||||
# Schema: schema.yaml v1.0.0
|
||||
#
|
||||
# SPC bridge. EVM chains focus.
|
||||
|
||||
id: spc
|
||||
name: "SPC Network"
|
||||
|
||||
hostnames:
|
||||
- bridge.spc.network
|
||||
|
||||
home_chain_id: 36911 # SPC C-Chain
|
||||
|
||||
supported_chains:
|
||||
- lux-c
|
||||
- ethereum
|
||||
- base
|
||||
|
||||
teleporter_addresses:
|
||||
"96369": "kms://teleport/lux-c/teleporter"
|
||||
"1": "kms://teleport/lux-c/teleporter-eth"
|
||||
"8453": "kms://teleport/lux-c/teleporter-eth"
|
||||
|
||||
branding:
|
||||
logo_url: "/static/spc/logo.svg"
|
||||
primary_color: "#3B82F6"
|
||||
theme: dark
|
||||
|
||||
features:
|
||||
swap: true
|
||||
yield: false
|
||||
perps: false
|
||||
@@ -0,0 +1,34 @@
|
||||
# Zoo Network — Tenant Manifest
|
||||
# Schema: schema.yaml v1.0.0
|
||||
#
|
||||
# Zoo Labs Foundation bridge. Subset of chains relevant to DeAI/DeSci.
|
||||
|
||||
id: zoo
|
||||
name: "Zoo Network"
|
||||
|
||||
hostnames:
|
||||
- bridge.zoo.ngo
|
||||
|
||||
home_chain_id: 200200 # Zoo C-Chain
|
||||
|
||||
supported_chains:
|
||||
- lux-c
|
||||
- ethereum
|
||||
- base
|
||||
- solana
|
||||
|
||||
teleporter_addresses:
|
||||
"96369": "kms://teleport/lux-c/teleporter"
|
||||
"1": "kms://teleport/lux-c/teleporter-eth"
|
||||
"8453": "kms://teleport/lux-c/teleporter-eth"
|
||||
"1399811149": "kms://teleport/lux-c/teleporter-sol"
|
||||
|
||||
branding:
|
||||
logo_url: "/static/zoo/logo.svg"
|
||||
primary_color: "#10B981"
|
||||
theme: dark
|
||||
|
||||
features:
|
||||
swap: true
|
||||
yield: true
|
||||
perps: false
|
||||
@@ -0,0 +1,10 @@
|
||||
FROM golang:1.24-alpine AS build
|
||||
WORKDIR /app
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
COPY . .
|
||||
RUN CGO_ENABLED=0 go build -o /opnet-watcher .
|
||||
|
||||
FROM alpine:3.21
|
||||
COPY --from=build /opnet-watcher /usr/local/bin/
|
||||
ENTRYPOINT ["opnet-watcher"]
|
||||
@@ -0,0 +1,127 @@
|
||||
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// API serves tenant-aware bridge configuration over HTTP.
|
||||
// All endpoints resolve the tenant from the Host header.
|
||||
type API struct {
|
||||
registry *TenantRegistry
|
||||
startTime time.Time
|
||||
mux *http.ServeMux
|
||||
}
|
||||
|
||||
// NewAPI creates the HTTP handler.
|
||||
func NewAPI(registry *TenantRegistry) *API {
|
||||
a := &API{
|
||||
registry: registry,
|
||||
startTime: time.Now(),
|
||||
mux: http.NewServeMux(),
|
||||
}
|
||||
a.mux.HandleFunc("GET /v1/config", a.handleConfig)
|
||||
a.mux.HandleFunc("GET /v1/chains", a.handleChains)
|
||||
a.mux.HandleFunc("GET /v1/assets", a.handleAssets)
|
||||
a.mux.HandleFunc("GET /v1/status", a.handleStatus)
|
||||
a.mux.HandleFunc("GET /health", a.handleHealth)
|
||||
return a
|
||||
}
|
||||
|
||||
// ServeHTTP implements http.Handler.
|
||||
func (a *API) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
a.mux.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// handleConfig returns the full tenant configuration for the requesting host.
|
||||
// GET /v1/config
|
||||
func (a *API) handleConfig(w http.ResponseWriter, r *http.Request) {
|
||||
t, err := a.registry.TenantForHostname(r.Host)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusNotFound, "unknown tenant")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, t)
|
||||
}
|
||||
|
||||
// handleChains returns the supported chains for the requesting tenant.
|
||||
// GET /v1/chains
|
||||
func (a *API) handleChains(w http.ResponseWriter, r *http.Request) {
|
||||
t, err := a.registry.TenantForHostname(r.Host)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusNotFound, "unknown tenant")
|
||||
return
|
||||
}
|
||||
chains, err := a.registry.ChainsForTenant(t.ID)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "chain resolution failed")
|
||||
log.Printf("api: chains error tenant=%s: %v", t.ID, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, chains)
|
||||
}
|
||||
|
||||
// handleAssets returns the supported assets for the requesting tenant.
|
||||
// GET /v1/assets
|
||||
func (a *API) handleAssets(w http.ResponseWriter, r *http.Request) {
|
||||
t, err := a.registry.TenantForHostname(r.Host)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusNotFound, "unknown tenant")
|
||||
return
|
||||
}
|
||||
assets, err := a.registry.AssetsForTenant(t.ID)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "asset resolution failed")
|
||||
log.Printf("api: assets error tenant=%s: %v", t.ID, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, assets)
|
||||
}
|
||||
|
||||
// statusResponse is the shape of GET /v1/status.
|
||||
type statusResponse struct {
|
||||
Tenant string `json:"tenant"`
|
||||
Healthy bool `json:"healthy"`
|
||||
Uptime string `json:"uptime"`
|
||||
}
|
||||
|
||||
// handleStatus returns bridge status for the requesting tenant.
|
||||
// GET /v1/status
|
||||
func (a *API) handleStatus(w http.ResponseWriter, r *http.Request) {
|
||||
t, err := a.registry.TenantForHostname(r.Host)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusNotFound, "unknown tenant")
|
||||
return
|
||||
}
|
||||
resp := statusResponse{
|
||||
Tenant: t.ID,
|
||||
Healthy: true,
|
||||
Uptime: time.Since(a.startTime).Truncate(time.Second).String(),
|
||||
}
|
||||
writeJSON(w, http.StatusOK, resp)
|
||||
}
|
||||
|
||||
// handleHealth is the K8s liveness/readiness probe.
|
||||
// GET /health
|
||||
func (a *API) handleHealth(w http.ResponseWriter, _ *http.Request) {
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
|
||||
}
|
||||
|
||||
// writeJSON encodes v as JSON and writes it to w.
|
||||
func writeJSON(w http.ResponseWriter, code int, v any) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(code)
|
||||
if err := json.NewEncoder(w).Encode(v); err != nil {
|
||||
log.Printf("api: json encode error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// writeError writes a JSON error response.
|
||||
func writeError(w http.ResponseWriter, code int, msg string) {
|
||||
writeJSON(w, code, map[string]string{"error": msg})
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func newTestAPI(t *testing.T) *API {
|
||||
t.Helper()
|
||||
tenantDir, chainDir := setupTestManifests(t)
|
||||
r := NewTenantRegistry(chainDir)
|
||||
if err := r.Load(tenantDir); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return NewAPI(r)
|
||||
}
|
||||
|
||||
func TestAPIConfig(t *testing.T) {
|
||||
api := newTestAPI(t)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/v1/config", nil)
|
||||
req.Host = "bridge.lux.network"
|
||||
rec := httptest.NewRecorder()
|
||||
api.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status %d, want 200", rec.Code)
|
||||
}
|
||||
var tenant Tenant
|
||||
if err := json.NewDecoder(rec.Body).Decode(&tenant); err != nil {
|
||||
t.Fatalf("decode: %v", err)
|
||||
}
|
||||
if tenant.ID != "lux" {
|
||||
t.Errorf("id = %q, want lux", tenant.ID)
|
||||
}
|
||||
if tenant.HomeChainID != 96369 {
|
||||
t.Errorf("home_chain_id = %d, want 96369", tenant.HomeChainID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPIConfigUnknownHost(t *testing.T) {
|
||||
api := newTestAPI(t)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/v1/config", nil)
|
||||
req.Host = "bridge.unknown.com"
|
||||
rec := httptest.NewRecorder()
|
||||
api.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusNotFound {
|
||||
t.Fatalf("status %d, want 404", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPIChains(t *testing.T) {
|
||||
api := newTestAPI(t)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/v1/chains", nil)
|
||||
req.Host = "bridge.lux.network"
|
||||
rec := httptest.NewRecorder()
|
||||
api.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status %d, want 200", rec.Code)
|
||||
}
|
||||
var chains []ChainManifest
|
||||
if err := json.NewDecoder(rec.Body).Decode(&chains); err != nil {
|
||||
t.Fatalf("decode: %v", err)
|
||||
}
|
||||
if len(chains) != 2 {
|
||||
t.Errorf("got %d chains, want 2", len(chains))
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPIChainsZoo(t *testing.T) {
|
||||
api := newTestAPI(t)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/v1/chains", nil)
|
||||
req.Host = "bridge.zoo.ngo"
|
||||
rec := httptest.NewRecorder()
|
||||
api.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status %d, want 200", rec.Code)
|
||||
}
|
||||
var chains []ChainManifest
|
||||
if err := json.NewDecoder(rec.Body).Decode(&chains); err != nil {
|
||||
t.Fatalf("decode: %v", err)
|
||||
}
|
||||
if len(chains) != 1 {
|
||||
t.Errorf("got %d chains, want 1", len(chains))
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPIAssets(t *testing.T) {
|
||||
api := newTestAPI(t)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/v1/assets", nil)
|
||||
req.Host = "bridge.lux.network"
|
||||
rec := httptest.NewRecorder()
|
||||
api.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status %d, want 200", rec.Code)
|
||||
}
|
||||
var assets []TenantAsset
|
||||
if err := json.NewDecoder(rec.Body).Decode(&assets); err != nil {
|
||||
t.Fatalf("decode: %v", err)
|
||||
}
|
||||
if len(assets) != 3 {
|
||||
t.Errorf("got %d assets, want 3", len(assets))
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPIStatus(t *testing.T) {
|
||||
api := newTestAPI(t)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/v1/status", nil)
|
||||
req.Host = "bridge.zoo.ngo"
|
||||
rec := httptest.NewRecorder()
|
||||
api.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status %d, want 200", rec.Code)
|
||||
}
|
||||
var status statusResponse
|
||||
if err := json.NewDecoder(rec.Body).Decode(&status); err != nil {
|
||||
t.Fatalf("decode: %v", err)
|
||||
}
|
||||
if status.Tenant != "zoo" {
|
||||
t.Errorf("tenant = %q, want zoo", status.Tenant)
|
||||
}
|
||||
if !status.Healthy {
|
||||
t.Error("expected healthy=true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPIHealth(t *testing.T) {
|
||||
api := newTestAPI(t)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/health", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
api.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status %d, want 200", rec.Code)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
// LuxChainID is the Lux mainnet chain ID.
|
||||
LuxChainID uint64 = 96369
|
||||
|
||||
// DefaultBlockPollInterval is how often we poll the source chain for new blocks.
|
||||
DefaultBlockPollInterval = 5 * time.Second
|
||||
|
||||
// DefaultBackingInterval is how often we submit backing attestations.
|
||||
DefaultBackingInterval = 12 * time.Hour
|
||||
)
|
||||
|
||||
// Config holds all runtime configuration for the watcher service.
|
||||
type Config struct {
|
||||
// Source chain RPC/API endpoint.
|
||||
SourceRPCURL string
|
||||
|
||||
// Lux C-Chain JSON-RPC endpoint.
|
||||
LuxRPCURL string
|
||||
|
||||
// Teleporter contract address on Lux C-Chain (hex, 0x-prefixed).
|
||||
TeleporterAddr string
|
||||
|
||||
// Source chain bridge contract/program address.
|
||||
SourceBridgeAddr string
|
||||
|
||||
// Solana bridge config PDA (precomputed findProgramAddress(["bridge_config"], programID)).
|
||||
// Required when --chain=solana.
|
||||
SolanaVaultAddr string
|
||||
|
||||
// Path to file containing the signer private key (hex-encoded, 32 bytes).
|
||||
// DEVELOPMENT ONLY -- in production use --threshold with --kms-endpoint.
|
||||
SignerKeyPath string
|
||||
|
||||
// NEW-02: Path to hex-encoded tx hot wallet key for transaction submission.
|
||||
// Required in threshold mode (MPC signs proofs, hot wallet submits txs).
|
||||
TxKeyPath string
|
||||
|
||||
// How often to poll the source chain for new blocks/slots.
|
||||
BlockPollInterval time.Duration
|
||||
|
||||
// How often to submit backing attestations.
|
||||
BackingInterval time.Duration
|
||||
|
||||
// Block/slot height to start indexing from. 0 = latest.
|
||||
StartBlock uint64
|
||||
|
||||
// CRITICAL-02: Number of confirmations before processing a block.
|
||||
// Prevents bridge drain via source chain reorgs. Default 6 (BTC-like).
|
||||
ConfirmationDepth uint64
|
||||
|
||||
// CRITICAL-04: Use threshold signing (CGGMP21) instead of single key.
|
||||
// Single-key mode is for development/testing ONLY.
|
||||
Threshold bool
|
||||
|
||||
// CRITICAL-04: KMS endpoint for loading key shares in threshold mode.
|
||||
// Example: https://kms.hanzo.ai
|
||||
KMSEndpoint string
|
||||
|
||||
// CRITICAL-04: KMS secret path for the key share.
|
||||
KMSKeyPath string
|
||||
|
||||
// CRITICAL-03: Lux C-Chain chain ID for EIP-155 signing.
|
||||
LuxChainIDOverride uint64
|
||||
|
||||
// HIGH-03: Maximum percentage change in backing per attestation.
|
||||
// Prevents unbacked minting via compromised RPC. Default 100 (100%).
|
||||
MaxBackingChangePct uint64
|
||||
}
|
||||
|
||||
// DefaultConfig returns a Config with sensible defaults.
|
||||
// RPC URLs and contract addresses must be overridden.
|
||||
func DefaultConfig() Config {
|
||||
return Config{
|
||||
LuxRPCURL: "https://api.lux.network/ext/bc/C/rpc",
|
||||
BlockPollInterval: DefaultBlockPollInterval,
|
||||
BackingInterval: DefaultBackingInterval,
|
||||
ConfirmationDepth: 6,
|
||||
MaxBackingChangePct: 100,
|
||||
LuxChainIDOverride: LuxChainID,
|
||||
}
|
||||
}
|
||||
|
||||
// Validate checks that all required fields are set.
|
||||
func (c *Config) Validate() error {
|
||||
if c.SourceRPCURL == "" {
|
||||
return errors.New("config: source-rpc is required")
|
||||
}
|
||||
if c.LuxRPCURL == "" {
|
||||
return errors.New("config: lux-rpc is required")
|
||||
}
|
||||
if c.TeleporterAddr == "" {
|
||||
return errors.New("config: teleporter-addr is required")
|
||||
}
|
||||
if c.SourceBridgeAddr == "" {
|
||||
return errors.New("config: source-addr is required")
|
||||
}
|
||||
if c.Threshold {
|
||||
if c.KMSEndpoint == "" {
|
||||
return errors.New("config: --kms-endpoint is required in threshold mode")
|
||||
}
|
||||
if c.KMSKeyPath == "" {
|
||||
return errors.New("config: --kms-key-path is required in threshold mode")
|
||||
}
|
||||
if c.TxKeyPath == "" {
|
||||
return errors.New("config: --tx-key-path is required in threshold mode (hot wallet for tx submission)")
|
||||
}
|
||||
} else {
|
||||
if c.SignerKeyPath == "" {
|
||||
return errors.New("config: signer-key-path is required (or use --threshold with --kms-endpoint)")
|
||||
}
|
||||
}
|
||||
if c.ConfirmationDepth == 0 {
|
||||
return errors.New("config: confirmation-depth must be > 0")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadKeyShareFromKMS fetches threshold key share and group key from KMS.
|
||||
// Returns (shareBytes, groupKeyBytes, error).
|
||||
func LoadKeyShareFromKMS(endpoint, keyPath string) ([]byte, []byte, error) {
|
||||
url := endpoint + "/v1/secrets/" + keyPath
|
||||
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("kms request: %w", err)
|
||||
}
|
||||
// KMS auth via service account token mounted at well-known path.
|
||||
tokenBytes, err := os.ReadFile("/var/run/secrets/hanzo/kms-token")
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("kms auth token: %w", err)
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+string(tokenBytes))
|
||||
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("kms fetch: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, nil, fmt.Errorf("kms: status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var kmsResp struct {
|
||||
Share string `json:"share"`
|
||||
GroupKey string `json:"group_key"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&kmsResp); err != nil {
|
||||
return nil, nil, fmt.Errorf("kms decode: %w", err)
|
||||
}
|
||||
|
||||
share, err := hex.DecodeString(kmsResp.Share)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("kms share hex: %w", err)
|
||||
}
|
||||
groupKey, err := hex.DecodeString(kmsResp.GroupKey)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("kms group key hex: %w", err)
|
||||
}
|
||||
|
||||
return share, groupKey, nil
|
||||
}
|
||||
|
||||
// LoadSignerKey reads the hex-encoded private key from disk.
|
||||
// Returns raw 32-byte key material. DEVELOPMENT ONLY.
|
||||
func LoadSignerKey(path string) ([]byte, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load signer key: %w", err)
|
||||
}
|
||||
// Strip whitespace/newlines
|
||||
hexStr := string(data)
|
||||
for len(hexStr) > 0 && (hexStr[len(hexStr)-1] == '\n' || hexStr[len(hexStr)-1] == '\r' || hexStr[len(hexStr)-1] == ' ') {
|
||||
hexStr = hexStr[:len(hexStr)-1]
|
||||
}
|
||||
// Strip 0x prefix if present
|
||||
if len(hexStr) >= 2 && hexStr[:2] == "0x" {
|
||||
hexStr = hexStr[2:]
|
||||
}
|
||||
key, err := hex.DecodeString(hexStr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load signer key: invalid hex: %w", err)
|
||||
}
|
||||
if len(key) != 32 {
|
||||
return nil, fmt.Errorf("load signer key: expected 32 bytes, got %d", len(key))
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
module github.com/luxfi/bridge/opnet-watcher
|
||||
|
||||
go 1.26.1
|
||||
|
||||
require (
|
||||
github.com/luxfi/crypto v1.17.38
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260215031811-a0ab0b218a81 // indirect
|
||||
github.com/cloudflare/circl v1.6.3 // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
|
||||
github.com/luxfi/cache v1.1.0 // indirect
|
||||
github.com/luxfi/ids v1.2.9 // indirect
|
||||
github.com/luxfi/log v1.4.1 // indirect
|
||||
github.com/luxfi/mock v0.1.0 // indirect
|
||||
github.com/luxfi/utils v1.1.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mr-tron/base58 v1.2.0 // indirect
|
||||
github.com/supranational/blst v0.3.16 // indirect
|
||||
go.uber.org/mock v0.6.0 // indirect
|
||||
golang.org/x/crypto v0.48.0 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||
)
|
||||
|
||||
// Use local module during development
|
||||
replace github.com/luxfi/crypto => ../../crypto
|
||||
@@ -0,0 +1,43 @@
|
||||
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260215031811-a0ab0b218a81 h1:TBzelXBdnzDy+HCrBMcomEnhrmigkWOI1/mIPCi2u4M=
|
||||
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260215031811-a0ab0b218a81/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI=
|
||||
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
|
||||
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8=
|
||||
github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
|
||||
github.com/luxfi/cache v1.1.0 h1:6LUyGGZ+rrMAJBbAU6+UwkcamXj3zsboRUodIof2Ong=
|
||||
github.com/luxfi/cache v1.1.0/go.mod h1:9GvlEEE9rFPaaWxvVpSPwW8ZMo2+8VMNNcuPa4AwzPg=
|
||||
github.com/luxfi/ids v1.2.9 h1:+yjdhXW99drnd2Zlp1u/p8k3G23W3/1btJQ4ogHawUI=
|
||||
github.com/luxfi/ids v1.2.9/go.mod h1:khJOEdOPxd22yn0jcVrnbX1ADa0GHn5Y74gvCzN5BYc=
|
||||
github.com/luxfi/log v1.4.1 h1:rIfFRodb9jrD/w7KayaUk0Oc+37PaQQdKEEMJCjR8gw=
|
||||
github.com/luxfi/log v1.4.1/go.mod h1:64IE3xRMJcpkQwnPUfJw3pDj7wU0kRS7BZ9wM7R72jk=
|
||||
github.com/luxfi/mock v0.1.0 h1:IwElfNu+T9sXvzFX6tudPDx1vqPuACRSRdxpD5lxW+o=
|
||||
github.com/luxfi/mock v0.1.0/go.mod h1:izF+9K0gGzFC9zERn6Po37v46eLdPB+EIsDjL3GLk+U=
|
||||
github.com/luxfi/utils v1.1.0 h1:ti7HvjNwJd4ILDMERJtOAWE9mF8l+zqDVkgWnF7Agic=
|
||||
github.com/luxfi/utils v1.1.0/go.mod h1:ABqhBdGNig0CaDcnNYldv1byS0BTNi5IKPbJSPF1p98=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
|
||||
github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/supranational/blst v0.3.16 h1:bTDadT+3fK497EvLdWRQEjiGnUtzJ7jjIUMF0jqwYhE=
|
||||
github.com/supranational/blst v0.3.16/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw=
|
||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
@@ -0,0 +1,240 @@
|
||||
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
// teleport-watcher bridges deposit events from any registered source chain
|
||||
// to the Teleporter contract on Lux C-Chain.
|
||||
//
|
||||
// Supports 30+ chains via plugin architecture:
|
||||
// - EVM chains (Ethereum, Base, Arbitrum, etc.) use the generic EVMPlugin
|
||||
// - Non-EVM chains (OP_NET, Solana, TON, Sui, etc.) use dedicated plugins
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// teleport-watcher --chain=opnet --source-rpc=... --source-addr=... ...
|
||||
// teleport-watcher --chain=ethereum --source-rpc=... --source-addr=... ...
|
||||
// teleport-watcher --chain=solana --source-rpc=... --source-addr=... ...
|
||||
//
|
||||
// It watches the source-chain bridge contract for deposit events, signs proofs,
|
||||
// and submits them to Teleporter.mintDeposit on Lux. It also periodically
|
||||
// attests the total backing locked on the source chain.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/luxfi/bridge/opnet-watcher/plugins"
|
||||
"github.com/luxfi/crypto/threshold"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cfg := DefaultConfig()
|
||||
|
||||
var (
|
||||
chainName string
|
||||
tenantDir string
|
||||
chainDir string
|
||||
apiAddr string
|
||||
)
|
||||
flag.StringVar(&chainName, "chain", "opnet", "Source chain plugin")
|
||||
flag.StringVar(&tenantDir, "tenant-dir", envOrDefault("BRIDGE_TENANT_DIR", ""), "Directory containing tenant manifests")
|
||||
flag.StringVar(&chainDir, "chain-dir", envOrDefault("BRIDGE_CHAIN_DIR", ""), "Directory containing chain manifests")
|
||||
flag.StringVar(&apiAddr, "api-addr", ":8090", "Listen address for the tenant API server")
|
||||
flag.StringVar(&cfg.SourceRPCURL, "source-rpc", "", "Source chain RPC/API URL")
|
||||
flag.StringVar(&cfg.LuxRPCURL, "lux-rpc", cfg.LuxRPCURL, "Lux C-Chain JSON-RPC URL")
|
||||
flag.StringVar(&cfg.TeleporterAddr, "teleporter-addr", "", "Teleporter contract address (0x-prefixed)")
|
||||
flag.StringVar(&cfg.SourceBridgeAddr, "source-addr", "", "Source chain bridge contract/program address")
|
||||
flag.StringVar(&cfg.SignerKeyPath, "signer-key-path", "", "Path to hex-encoded signer private key")
|
||||
flag.StringVar(&cfg.TxKeyPath, "tx-key-path", "", "Path to hex-encoded tx hot wallet key (required with --threshold)")
|
||||
flag.DurationVar(&cfg.BlockPollInterval, "poll-interval", cfg.BlockPollInterval, "Block poll interval")
|
||||
flag.DurationVar(&cfg.BackingInterval, "backing-interval", cfg.BackingInterval, "Backing attestation interval")
|
||||
flag.Uint64Var(&cfg.StartBlock, "start-block", 0, "Block/slot height to start indexing from (0 = latest)")
|
||||
flag.Uint64Var(&cfg.ConfirmationDepth, "confirmation-depth", cfg.ConfirmationDepth, "Required block confirmations before processing (default 6)")
|
||||
flag.BoolVar(&cfg.Threshold, "threshold", false, "Use threshold signing (CGGMP21) — required for production")
|
||||
flag.StringVar(&cfg.KMSEndpoint, "kms-endpoint", "", "KMS endpoint for key share retrieval (required with --threshold)")
|
||||
flag.StringVar(&cfg.KMSKeyPath, "kms-key-path", "", "KMS secret path for key share (required with --threshold)")
|
||||
flag.Uint64Var(&cfg.MaxBackingChangePct, "max-backing-change-pct", cfg.MaxBackingChangePct, "Max backing change per attestation in percent (default 100)")
|
||||
flag.Uint64Var(&cfg.LuxChainIDOverride, "lux-chain-id", cfg.LuxChainIDOverride, "Lux C-Chain ID for EIP-155 signing")
|
||||
flag.StringVar(&cfg.SolanaVaultAddr, "solana-vault-addr", "", "Solana bridge config PDA address (required with --chain=solana)")
|
||||
|
||||
// Legacy OP_NET flags (backwards compatibility).
|
||||
flag.StringVar(&cfg.SourceRPCURL, "opnet-rpc", "", "OP_NET node JSON-RPC URL (legacy, use --source-rpc)")
|
||||
flag.StringVar(&cfg.SourceBridgeAddr, "opnet-bridge-addr", "", "OP_NET bridge address (legacy, use --source-addr)")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
if err := cfg.Validate(); err != nil {
|
||||
log.Fatalf("invalid config: %v", err)
|
||||
}
|
||||
|
||||
// Select chain plugin.
|
||||
plugin := selectPlugin(chainName, cfg)
|
||||
|
||||
var signer *Signer
|
||||
var txSigner *Signer // Hot wallet for tx submission
|
||||
if cfg.Threshold {
|
||||
shareBytes, groupKeyBytes, err := LoadKeyShareFromKMS(cfg.KMSEndpoint, cfg.KMSKeyPath)
|
||||
if err != nil {
|
||||
log.Fatalf("load key share from KMS: %v", err)
|
||||
}
|
||||
signer, err = NewThresholdSigner(threshold.SchemeCMP, shareBytes, groupKeyBytes)
|
||||
if err != nil {
|
||||
log.Fatalf("create threshold signer: %v", err)
|
||||
}
|
||||
// NEW-02: Threshold mode requires a separate hot wallet for tx submission.
|
||||
txKeyBytes, err := LoadSignerKey(cfg.TxKeyPath)
|
||||
if err != nil {
|
||||
log.Fatalf("load tx key: %v (--tx-key-path required with --threshold)", err)
|
||||
}
|
||||
txSigner, err = NewSigner(txKeyBytes)
|
||||
if err != nil {
|
||||
log.Fatalf("create tx signer: %v", err)
|
||||
}
|
||||
log.Printf("chain=%s chain_id=%d signer=0x%x tx=0x%x mode=threshold",
|
||||
plugin.Name(), plugin.ChainID(), signer.Address(), txSigner.Address())
|
||||
} else {
|
||||
log.Println("WARNING: running in single-key mode — DEVELOPMENT ONLY, not safe for production")
|
||||
keyBytes, err := LoadSignerKey(cfg.SignerKeyPath)
|
||||
if err != nil {
|
||||
log.Fatalf("load signer key: %v", err)
|
||||
}
|
||||
signer, err = NewSigner(keyBytes)
|
||||
if err != nil {
|
||||
log.Fatalf("create signer: %v", err)
|
||||
}
|
||||
txSigner = signer // Single-key mode: same key for proofs and txs
|
||||
log.Printf("chain=%s chain_id=%d signer=0x%x mode=single-key-DEV-ONLY", plugin.Name(), plugin.ChainID(), signer.Address())
|
||||
}
|
||||
|
||||
relay := NewRelay(cfg.LuxRPCURL, cfg.TeleporterAddr, signer, txSigner, cfg.LuxChainIDOverride)
|
||||
watcher := NewWatcher(cfg, plugin, signer, relay)
|
||||
|
||||
// Graceful shutdown on SIGINT/SIGTERM.
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
||||
go func() {
|
||||
sig := <-sigCh
|
||||
log.Printf("received signal %v, shutting down", sig)
|
||||
cancel()
|
||||
}()
|
||||
|
||||
// Start tenant API server if manifests are configured.
|
||||
if tenantDir != "" && chainDir != "" {
|
||||
registry := NewTenantRegistry(chainDir)
|
||||
if err := registry.Load(tenantDir); err != nil {
|
||||
log.Fatalf("load tenant manifests: %v", err)
|
||||
}
|
||||
for _, t := range registry.AllTenants() {
|
||||
log.Printf("tenant: id=%s home=%d hosts=%v chains=%d",
|
||||
t.ID, t.HomeChainID, t.Hostnames, len(t.SupportedChains))
|
||||
}
|
||||
api := NewAPI(registry)
|
||||
srv := &http.Server{Addr: apiAddr, Handler: api}
|
||||
go func() {
|
||||
log.Printf("api: listening on %s", apiAddr)
|
||||
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
log.Fatalf("api server: %v", err)
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
shutCtx, shutCancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer shutCancel()
|
||||
_ = srv.Shutdown(shutCtx)
|
||||
}()
|
||||
}
|
||||
|
||||
if err := watcher.Run(ctx); err != nil && err != context.Canceled {
|
||||
log.Fatalf("watcher exited with error: %v", err)
|
||||
}
|
||||
|
||||
// Give goroutines a moment to drain.
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
log.Printf("teleport-watcher stopped (chain=%s)", plugin.Name())
|
||||
}
|
||||
|
||||
func envOrDefault(key, fallback string) string {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return v
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
// evmChains maps chain names to their Teleporter chain IDs.
|
||||
// All use the generic EVMPlugin (eth_getLogs + eth_call).
|
||||
var evmChains = map[string]uint64{
|
||||
"ethereum": 1,
|
||||
"base": 8453,
|
||||
"avalanche": 43114,
|
||||
"arbitrum": 42161,
|
||||
"optimism": 10,
|
||||
"polygon": 137,
|
||||
"bsc": 56,
|
||||
"tron": 728126,
|
||||
"hyperliquid": 999,
|
||||
"filecoin": 314,
|
||||
"lux-c": 96369,
|
||||
"lux-zoo": 200200,
|
||||
"lux-hanzo": 36963,
|
||||
"lux-pars": 494949,
|
||||
"lux-spc": 36911,
|
||||
}
|
||||
|
||||
func selectPlugin(name string, cfg Config) plugins.ChainPlugin {
|
||||
// EVM chains — generic plugin.
|
||||
if chainID, ok := evmChains[name]; ok {
|
||||
return plugins.NewEVMPlugin(name, chainID, cfg.SourceRPCURL, cfg.SourceBridgeAddr)
|
||||
}
|
||||
|
||||
// Non-EVM chains — dedicated plugins.
|
||||
switch name {
|
||||
case "opnet":
|
||||
return plugins.NewOPNETPlugin(cfg.SourceRPCURL, cfg.SourceBridgeAddr)
|
||||
case "solana":
|
||||
return plugins.NewSolanaPlugin(cfg.SourceRPCURL, cfg.SourceBridgeAddr, cfg.SolanaVaultAddr)
|
||||
case "ton":
|
||||
return plugins.NewTONPlugin(cfg.SourceRPCURL, cfg.SourceBridgeAddr)
|
||||
case "sui":
|
||||
return plugins.NewSuiPlugin(cfg.SourceRPCURL, cfg.SourceBridgeAddr)
|
||||
case "near":
|
||||
return plugins.NewNEARPlugin(cfg.SourceRPCURL, cfg.SourceBridgeAddr)
|
||||
case "cosmos":
|
||||
return plugins.NewCosmosPlugin(cfg.SourceRPCURL, cfg.SourceBridgeAddr)
|
||||
case "xrpl":
|
||||
return plugins.NewXRPLPlugin(cfg.SourceRPCURL, cfg.SourceBridgeAddr)
|
||||
case "stellar":
|
||||
return plugins.NewStellarPlugin(cfg.SourceRPCURL, cfg.SourceBridgeAddr)
|
||||
case "cardano":
|
||||
return plugins.NewCardanoPlugin(cfg.SourceRPCURL, cfg.SourceBridgeAddr, envOrDefault("CHAIN_API_KEY", ""))
|
||||
case "algorand":
|
||||
return plugins.NewAlgorandPlugin(cfg.SourceRPCURL, cfg.SourceBridgeAddr, envOrDefault("CHAIN_API_KEY", ""))
|
||||
case "icp":
|
||||
return plugins.NewICPPlugin(cfg.SourceRPCURL, cfg.SourceBridgeAddr)
|
||||
case "polkadot":
|
||||
return plugins.NewPolkadotPlugin(cfg.SourceRPCURL, cfg.SourceBridgeAddr)
|
||||
case "aptos":
|
||||
return plugins.NewAptosPlugin(cfg.SourceRPCURL, cfg.SourceBridgeAddr)
|
||||
case "starknet":
|
||||
return plugins.NewStarkNetPlugin(cfg.SourceRPCURL, cfg.SourceBridgeAddr)
|
||||
case "stacks":
|
||||
return plugins.NewStacksPlugin(cfg.SourceRPCURL, cfg.SourceBridgeAddr)
|
||||
case "fuel":
|
||||
return plugins.NewFuelPlugin(cfg.SourceRPCURL, cfg.SourceBridgeAddr)
|
||||
case "tezos":
|
||||
return plugins.NewTezosPlugin(cfg.SourceRPCURL, cfg.SourceBridgeAddr)
|
||||
case "lux":
|
||||
return plugins.NewLuxPlugin(cfg.SourceRPCURL, cfg.SourceBridgeAddr)
|
||||
default:
|
||||
log.Fatalf("unknown chain: %s", name)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
Executable
BIN
Binary file not shown.
@@ -0,0 +1,43 @@
|
||||
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
const AlgorandChainID uint64 = 0x414C474F // "ALGO" in Teleporter namespace
|
||||
|
||||
// AlgorandPlugin polls the Algod API for deposit transactions from the
|
||||
// bridge application. Algorand uses ed25519 signing.
|
||||
type AlgorandPlugin struct {
|
||||
apiURL string
|
||||
appID string // Application ID
|
||||
apiKey string
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func NewAlgorandPlugin(apiURL, appID, apiKey string) *AlgorandPlugin {
|
||||
return &AlgorandPlugin{
|
||||
apiURL: apiURL,
|
||||
appID: appID,
|
||||
apiKey: apiKey,
|
||||
client: &http.Client{Timeout: 15 * time.Second},
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AlgorandPlugin) Name() string { return "algorand" }
|
||||
func (a *AlgorandPlugin) ChainID() uint64 { return AlgorandChainID }
|
||||
|
||||
func (a *AlgorandPlugin) PollDeposits(ctx context.Context, fromRound uint64) ([]DepositEvent, uint64, error) {
|
||||
return nil, fromRound, fmt.Errorf("algorand: not yet implemented")
|
||||
}
|
||||
|
||||
func (a *AlgorandPlugin) QueryBacking(ctx context.Context) (*big.Int, error) {
|
||||
return nil, fmt.Errorf("algorand: not yet implemented")
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
const AptosChainID uint64 = 0x41505400 // "APT\x00" in Teleporter namespace
|
||||
|
||||
// AptosPlugin polls the Aptos REST API for deposit events from the bridge
|
||||
// Move module. Aptos uses ed25519 signing.
|
||||
type AptosPlugin struct {
|
||||
apiURL string
|
||||
bridgeAddr string // Module address (0x-prefixed)
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func NewAptosPlugin(apiURL, bridgeAddr string) *AptosPlugin {
|
||||
return &AptosPlugin{
|
||||
apiURL: apiURL,
|
||||
bridgeAddr: bridgeAddr,
|
||||
client: &http.Client{Timeout: 15 * time.Second},
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AptosPlugin) Name() string { return "aptos" }
|
||||
func (a *AptosPlugin) ChainID() uint64 { return AptosChainID }
|
||||
|
||||
func (a *AptosPlugin) PollDeposits(ctx context.Context, fromVersion uint64) ([]DepositEvent, uint64, error) {
|
||||
return nil, fromVersion, fmt.Errorf("aptos: not yet implemented")
|
||||
}
|
||||
|
||||
func (a *AptosPlugin) QueryBacking(ctx context.Context) (*big.Int, error) {
|
||||
return nil, fmt.Errorf("aptos: not yet implemented")
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
const CardanoChainID uint64 = 0x41444100 // "ADA\x00" in Teleporter namespace
|
||||
|
||||
// CardanoPlugin polls the Blockfrost API for deposit transactions
|
||||
// from the bridge script address. Cardano uses ed25519 signing.
|
||||
type CardanoPlugin struct {
|
||||
apiURL string
|
||||
bridgeAddr string // Bech32 script address
|
||||
apiKey string
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func NewCardanoPlugin(apiURL, bridgeAddr, apiKey string) *CardanoPlugin {
|
||||
return &CardanoPlugin{
|
||||
apiURL: apiURL,
|
||||
bridgeAddr: bridgeAddr,
|
||||
apiKey: apiKey,
|
||||
client: &http.Client{Timeout: 15 * time.Second},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CardanoPlugin) Name() string { return "cardano" }
|
||||
func (c *CardanoPlugin) ChainID() uint64 { return CardanoChainID }
|
||||
|
||||
func (c *CardanoPlugin) PollDeposits(ctx context.Context, fromSlot uint64) ([]DepositEvent, uint64, error) {
|
||||
return nil, fromSlot, fmt.Errorf("cardano: not yet implemented")
|
||||
}
|
||||
|
||||
func (c *CardanoPlugin) QueryBacking(ctx context.Context) (*big.Int, error) {
|
||||
return nil, fmt.Errorf("cardano: not yet implemented")
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
const CosmosChainID uint64 = 0x434F534D // "COSM" in Teleporter namespace
|
||||
|
||||
// CosmosPlugin polls a Cosmos/IBC Tendermint RPC node for deposit events
|
||||
// from the bridge module. Uses secp256k1 signing and IBC event attributes.
|
||||
type CosmosPlugin struct {
|
||||
rpcURL string
|
||||
bridgeAddr string // Bech32 contract address
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func NewCosmosPlugin(rpcURL, bridgeAddr string) *CosmosPlugin {
|
||||
return &CosmosPlugin{
|
||||
rpcURL: rpcURL,
|
||||
bridgeAddr: bridgeAddr,
|
||||
client: &http.Client{Timeout: 15 * time.Second},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CosmosPlugin) Name() string { return "cosmos" }
|
||||
func (c *CosmosPlugin) ChainID() uint64 { return CosmosChainID }
|
||||
|
||||
func (c *CosmosPlugin) PollDeposits(ctx context.Context, fromBlock uint64) ([]DepositEvent, uint64, error) {
|
||||
return nil, fromBlock, fmt.Errorf("cosmos: not yet implemented")
|
||||
}
|
||||
|
||||
func (c *CosmosPlugin) QueryBacking(ctx context.Context) (*big.Int, error) {
|
||||
return nil, fmt.Errorf("cosmos: not yet implemented")
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Deposit(address recipient, uint256 amount, uint64 nonce)
|
||||
// keccak256 topic: 0xe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c
|
||||
const evmDepositTopic = "0xe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c"
|
||||
|
||||
// EVMPlugin is a generic EVM chain plugin that polls eth_getLogs for Deposit
|
||||
// events from a vault contract. Works for any EVM-compatible chain.
|
||||
type EVMPlugin struct {
|
||||
name string
|
||||
chainID uint64
|
||||
rpcURL string
|
||||
vaultAddr string
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
// NewEVMPlugin creates a plugin for any EVM chain. The name and chainID
|
||||
// identify the chain in the Teleporter namespace; rpcURL is the JSON-RPC
|
||||
// endpoint; vaultAddr is the bridge vault contract address.
|
||||
func NewEVMPlugin(name string, chainID uint64, rpcURL, vaultAddr string) *EVMPlugin {
|
||||
return &EVMPlugin{
|
||||
name: name,
|
||||
chainID: chainID,
|
||||
rpcURL: rpcURL,
|
||||
vaultAddr: vaultAddr,
|
||||
client: &http.Client{Timeout: 15 * time.Second},
|
||||
}
|
||||
}
|
||||
|
||||
func (e *EVMPlugin) Name() string { return e.name }
|
||||
func (e *EVMPlugin) ChainID() uint64 { return e.chainID }
|
||||
|
||||
// PollDeposits fetches Deposit event logs from the vault contract between
|
||||
// fromBlock+1 and the current block height via eth_getLogs.
|
||||
func (e *EVMPlugin) PollDeposits(ctx context.Context, fromBlock uint64) ([]DepositEvent, uint64, error) {
|
||||
height, err := e.blockNumber(ctx)
|
||||
if err != nil {
|
||||
return nil, fromBlock, fmt.Errorf("%s: eth_blockNumber: %w", e.name, err)
|
||||
}
|
||||
if height <= fromBlock {
|
||||
return nil, fromBlock, nil
|
||||
}
|
||||
|
||||
params := map[string]interface{}{
|
||||
"fromBlock": fmt.Sprintf("0x%x", fromBlock+1),
|
||||
"toBlock": fmt.Sprintf("0x%x", height),
|
||||
"address": e.vaultAddr,
|
||||
"topics": [][]string{{evmDepositTopic}},
|
||||
}
|
||||
result, err := e.rpcCall(ctx, "eth_getLogs", []interface{}{params})
|
||||
if err != nil {
|
||||
return nil, fromBlock, fmt.Errorf("%s: eth_getLogs: %w", e.name, err)
|
||||
}
|
||||
|
||||
var logs []struct {
|
||||
TxHash string `json:"transactionHash"`
|
||||
BlockNumber string `json:"blockNumber"`
|
||||
Data string `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(result, &logs); err != nil {
|
||||
return nil, fromBlock, fmt.Errorf("%s: parse logs: %w", e.name, err)
|
||||
}
|
||||
|
||||
var events []DepositEvent
|
||||
for _, l := range logs {
|
||||
data, err := hexToBytes(l.Data)
|
||||
if err != nil {
|
||||
log.Printf("WARN: %s: skip malformed log in tx %s: %v", e.name, l.TxHash, err)
|
||||
continue
|
||||
}
|
||||
// Deposit event data layout: recipient (32 bytes, address in last 20)
|
||||
// + amount (32 bytes) + nonce (32 bytes) = 96 bytes minimum.
|
||||
if len(data) < 96 {
|
||||
log.Printf("WARN: %s: skip short log data (%d bytes) in tx %s", e.name, len(data), l.TxHash)
|
||||
continue
|
||||
}
|
||||
|
||||
var recipient [20]byte
|
||||
copy(recipient[:], data[12:32])
|
||||
amount := new(big.Int).SetBytes(data[32:64])
|
||||
nonce := new(big.Int).SetBytes(data[64:96]).Uint64()
|
||||
|
||||
blkNum := new(big.Int)
|
||||
blkNum.SetString(l.BlockNumber, 0)
|
||||
|
||||
events = append(events, DepositEvent{
|
||||
SrcChainID: e.chainID,
|
||||
Nonce: nonce,
|
||||
Recipient: recipient,
|
||||
Amount: amount,
|
||||
TxID: l.TxHash,
|
||||
BlockHeight: blkNum.Uint64(),
|
||||
})
|
||||
}
|
||||
return events, height, nil
|
||||
}
|
||||
|
||||
// QueryBacking calls totalLocked() on the vault contract via eth_call.
|
||||
func (e *EVMPlugin) QueryBacking(ctx context.Context) (*big.Int, error) {
|
||||
// totalLocked() selector: keccak256("totalLocked()")[:4]
|
||||
params := map[string]string{
|
||||
"to": e.vaultAddr,
|
||||
"data": "0xaf29611e",
|
||||
}
|
||||
result, err := e.rpcCall(ctx, "eth_call", []interface{}{params, "latest"})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: eth_call totalLocked: %w", e.name, err)
|
||||
}
|
||||
|
||||
var hexResult string
|
||||
if err := json.Unmarshal(result, &hexResult); err != nil {
|
||||
return nil, fmt.Errorf("%s: parse totalLocked: %w", e.name, err)
|
||||
}
|
||||
|
||||
data, err := hexToBytes(hexResult)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: decode totalLocked: %w", e.name, err)
|
||||
}
|
||||
if len(data) < 32 {
|
||||
return nil, fmt.Errorf("%s: totalLocked too short: %d bytes", e.name, len(data))
|
||||
}
|
||||
return new(big.Int).SetBytes(data[:32]), nil
|
||||
}
|
||||
|
||||
func (e *EVMPlugin) blockNumber(ctx context.Context) (uint64, error) {
|
||||
result, err := e.rpcCall(ctx, "eth_blockNumber", nil)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
var hex string
|
||||
if err := json.Unmarshal(result, &hex); err != nil {
|
||||
return 0, fmt.Errorf("parse blockNumber: %w", err)
|
||||
}
|
||||
n := new(big.Int)
|
||||
n.SetString(hex, 0)
|
||||
return n.Uint64(), nil
|
||||
}
|
||||
|
||||
func (e *EVMPlugin) rpcCall(ctx context.Context, method string, params interface{}) (json.RawMessage, error) {
|
||||
reqBody := map[string]interface{}{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": method,
|
||||
"params": params,
|
||||
}
|
||||
body, err := json.Marshal(reqBody)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, e.rpcURL, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := e.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var rpcResp struct {
|
||||
Result json.RawMessage `json:"result"`
|
||||
Error *struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
} `json:"error"`
|
||||
}
|
||||
if err := json.Unmarshal(respBody, &rpcResp); err != nil {
|
||||
return nil, fmt.Errorf("rpc decode: %w", err)
|
||||
}
|
||||
if rpcResp.Error != nil {
|
||||
return nil, fmt.Errorf("rpc error %d: %s", rpcResp.Error.Code, rpcResp.Error.Message)
|
||||
}
|
||||
return rpcResp.Result, nil
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
const FuelChainID uint64 = 4294967520
|
||||
|
||||
// FuelPlugin polls a Fuel GraphQL API for deposit receipts from the bridge
|
||||
// Sway contract. Fuel uses secp256k1 signing.
|
||||
type FuelPlugin struct {
|
||||
apiURL string
|
||||
bridgeAddr string // Fuel contract ID (0x-prefixed)
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func NewFuelPlugin(apiURL, bridgeAddr string) *FuelPlugin {
|
||||
return &FuelPlugin{
|
||||
apiURL: apiURL,
|
||||
bridgeAddr: bridgeAddr,
|
||||
client: &http.Client{Timeout: 15 * time.Second},
|
||||
}
|
||||
}
|
||||
|
||||
func (f *FuelPlugin) Name() string { return "fuel" }
|
||||
func (f *FuelPlugin) ChainID() uint64 { return FuelChainID }
|
||||
|
||||
func (f *FuelPlugin) PollDeposits(ctx context.Context, fromBlock uint64) ([]DepositEvent, uint64, error) {
|
||||
return nil, fromBlock, fmt.Errorf("fuel: not yet implemented")
|
||||
}
|
||||
|
||||
func (f *FuelPlugin) QueryBacking(ctx context.Context) (*big.Int, error) {
|
||||
return nil, fmt.Errorf("fuel: not yet implemented")
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
const ICPChainID uint64 = 0x49435000 // "ICP\x00" in Teleporter namespace
|
||||
|
||||
// ICPPlugin polls the ICP Agent API for deposit events from the bridge
|
||||
// canister. ICP uses secp256k1 for canister signatures.
|
||||
type ICPPlugin struct {
|
||||
apiURL string
|
||||
canisterID string
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func NewICPPlugin(apiURL, canisterID string) *ICPPlugin {
|
||||
return &ICPPlugin{
|
||||
apiURL: apiURL,
|
||||
canisterID: canisterID,
|
||||
client: &http.Client{Timeout: 15 * time.Second},
|
||||
}
|
||||
}
|
||||
|
||||
func (i *ICPPlugin) Name() string { return "icp" }
|
||||
func (i *ICPPlugin) ChainID() uint64 { return ICPChainID }
|
||||
|
||||
func (i *ICPPlugin) PollDeposits(ctx context.Context, fromBlock uint64) ([]DepositEvent, uint64, error) {
|
||||
return nil, fromBlock, fmt.Errorf("icp: not yet implemented")
|
||||
}
|
||||
|
||||
func (i *ICPPlugin) QueryBacking(ctx context.Context) (*big.Int, error) {
|
||||
return nil, fmt.Errorf("icp: not yet implemented")
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
const LuxChainID uint64 = 0x4C555800 // "LUX\x00" in Teleporter namespace
|
||||
|
||||
// LuxPlugin watches for Lux Warp messages from the bridge contract on
|
||||
// Lux C-Chain. Unlike MPC-signed chains, Lux uses native Warp message
|
||||
// verification -- no threshold signature is needed.
|
||||
type LuxPlugin struct {
|
||||
rpcURL string
|
||||
bridgeAddr string // Lux C-Chain vault address
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func NewLuxPlugin(rpcURL, bridgeAddr string) *LuxPlugin {
|
||||
return &LuxPlugin{
|
||||
rpcURL: rpcURL,
|
||||
bridgeAddr: bridgeAddr,
|
||||
client: &http.Client{Timeout: 15 * time.Second},
|
||||
}
|
||||
}
|
||||
|
||||
func (l *LuxPlugin) Name() string { return "lux" }
|
||||
func (l *LuxPlugin) ChainID() uint64 { return LuxChainID }
|
||||
|
||||
func (l *LuxPlugin) PollDeposits(ctx context.Context, fromBlock uint64) ([]DepositEvent, uint64, error) {
|
||||
return nil, fromBlock, fmt.Errorf("lux: not yet implemented (warp message verification)")
|
||||
}
|
||||
|
||||
func (l *LuxPlugin) QueryBacking(ctx context.Context) (*big.Int, error) {
|
||||
return nil, fmt.Errorf("lux: not yet implemented (warp message verification)")
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
const NEARChainID uint64 = 0x4E454152 // "NEAR" in Teleporter namespace
|
||||
|
||||
// NEARPlugin polls a NEAR JSON-RPC node for deposit receipts from the bridge contract.
|
||||
// NEAR uses ed25519 signing and account-based state.
|
||||
type NEARPlugin struct {
|
||||
rpcURL string
|
||||
bridgeAddr string // NEAR account ID (e.g., bridge.lux.near)
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func NewNEARPlugin(rpcURL, bridgeAddr string) *NEARPlugin {
|
||||
return &NEARPlugin{
|
||||
rpcURL: rpcURL,
|
||||
bridgeAddr: bridgeAddr,
|
||||
client: &http.Client{Timeout: 15 * time.Second},
|
||||
}
|
||||
}
|
||||
|
||||
func (n *NEARPlugin) Name() string { return "near" }
|
||||
func (n *NEARPlugin) ChainID() uint64 { return NEARChainID }
|
||||
|
||||
func (n *NEARPlugin) PollDeposits(ctx context.Context, fromBlock uint64) ([]DepositEvent, uint64, error) {
|
||||
return nil, fromBlock, fmt.Errorf("near: not yet implemented")
|
||||
}
|
||||
|
||||
func (n *NEARPlugin) QueryBacking(ctx context.Context) (*big.Int, error) {
|
||||
return nil, fmt.Errorf("near: not yet implemented")
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
const OPNETChainID uint64 = 4294967299 // 0x100000003 in Teleporter namespace
|
||||
|
||||
// OPNETPlugin polls an OP_NET node for Lock events from the bridge contract
|
||||
// and converts them to canonical DepositEvent format.
|
||||
type OPNETPlugin struct {
|
||||
rpcURL string
|
||||
bridgeAddr string
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func NewOPNETPlugin(rpcURL, bridgeAddr string) *OPNETPlugin {
|
||||
return &OPNETPlugin{
|
||||
rpcURL: rpcURL,
|
||||
bridgeAddr: bridgeAddr,
|
||||
client: &http.Client{Timeout: 10 * time.Second},
|
||||
}
|
||||
}
|
||||
|
||||
func (o *OPNETPlugin) Name() string { return "opnet" }
|
||||
func (o *OPNETPlugin) ChainID() uint64 { return OPNETChainID }
|
||||
|
||||
// PollDeposits fetches Lock events from the OP_NET bridge contract between
|
||||
// fromBlock+1 and the current block height.
|
||||
func (o *OPNETPlugin) PollDeposits(ctx context.Context, fromBlock uint64) ([]DepositEvent, uint64, error) {
|
||||
height, err := o.getBlockHeight(ctx)
|
||||
if err != nil {
|
||||
return nil, fromBlock, fmt.Errorf("opnet: get height: %w", err)
|
||||
}
|
||||
if height <= fromBlock {
|
||||
return nil, fromBlock, nil
|
||||
}
|
||||
|
||||
var events []DepositEvent
|
||||
for block := fromBlock + 1; block <= height; block++ {
|
||||
blockEvents, err := o.getBlockDeposits(ctx, block)
|
||||
if err != nil {
|
||||
return events, block - 1, fmt.Errorf("opnet: block %d: %w", block, err)
|
||||
}
|
||||
events = append(events, blockEvents...)
|
||||
}
|
||||
return events, height, nil
|
||||
}
|
||||
|
||||
// QueryBacking calls totalLocked() on the OP_NET bridge contract.
|
||||
func (o *OPNETPlugin) QueryBacking(ctx context.Context) (*big.Int, error) {
|
||||
// totalLocked() selector
|
||||
selector := "0xaf29611e" // keccak256("totalLocked()")[:4]
|
||||
|
||||
params := map[string]string{
|
||||
"to": o.bridgeAddr,
|
||||
"data": selector,
|
||||
}
|
||||
result, err := o.rpcCall(ctx, "opnet_call", []interface{}{params, "latest"})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("query totalLocked: %w", err)
|
||||
}
|
||||
|
||||
var hexResult string
|
||||
if err := json.Unmarshal(result, &hexResult); err != nil {
|
||||
return nil, fmt.Errorf("parse totalLocked: %w", err)
|
||||
}
|
||||
|
||||
data, err := hexToBytes(hexResult)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decode totalLocked: %w", err)
|
||||
}
|
||||
if len(data) < 32 {
|
||||
return nil, fmt.Errorf("totalLocked too short: %d bytes", len(data))
|
||||
}
|
||||
return new(big.Int).SetBytes(data[:32]), nil
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────
|
||||
// OP_NET RPC helpers
|
||||
// ────────────────────────────────────────────────────────────────────
|
||||
|
||||
func (o *OPNETPlugin) getBlockHeight(ctx context.Context) (uint64, error) {
|
||||
resp, err := o.rpcCall(ctx, "opnet_blockNumber", nil)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
var height uint64
|
||||
if err := json.Unmarshal(resp, &height); err != nil {
|
||||
var hexHeight string
|
||||
if err2 := json.Unmarshal(resp, &hexHeight); err2 != nil {
|
||||
return 0, fmt.Errorf("parse height: %w", err)
|
||||
}
|
||||
n := new(big.Int)
|
||||
n.SetString(hexHeight, 0)
|
||||
height = n.Uint64()
|
||||
}
|
||||
return height, nil
|
||||
}
|
||||
|
||||
func (o *OPNETPlugin) getBlockDeposits(ctx context.Context, blockNum uint64) ([]DepositEvent, error) {
|
||||
lockTopic := "0x6c6f636b00000000000000000000000000000000000000000000000000000000"
|
||||
|
||||
params := map[string]interface{}{
|
||||
"fromBlock": fmt.Sprintf("0x%x", blockNum),
|
||||
"toBlock": fmt.Sprintf("0x%x", blockNum),
|
||||
"address": o.bridgeAddr,
|
||||
"topics": [][]string{{lockTopic}},
|
||||
}
|
||||
|
||||
resp, err := o.rpcCall(ctx, "opnet_getLogs", []interface{}{params})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var logs []struct {
|
||||
TxHash string `json:"transactionHash"`
|
||||
Topics []string `json:"topics"`
|
||||
Data string `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(resp, &logs); err != nil {
|
||||
return nil, fmt.Errorf("parse logs: %w", err)
|
||||
}
|
||||
|
||||
var events []DepositEvent
|
||||
for _, l := range logs {
|
||||
data, err := hexToBytes(l.Data)
|
||||
if err != nil {
|
||||
log.Printf("WARN: skipping malformed event at block %d: hex decode error: %v", blockNum, err)
|
||||
continue
|
||||
}
|
||||
if len(data) < 160 {
|
||||
log.Printf("WARN: skipping malformed event at block %d: data length %d < 160", blockNum, len(data))
|
||||
continue
|
||||
}
|
||||
|
||||
nonce := new(big.Int).SetBytes(data[64:96]).Uint64()
|
||||
var recipient [20]byte
|
||||
copy(recipient[:], data[108:128])
|
||||
amount := new(big.Int).SetBytes(data[128:160])
|
||||
|
||||
events = append(events, DepositEvent{
|
||||
SrcChainID: OPNETChainID,
|
||||
Nonce: nonce,
|
||||
Recipient: recipient,
|
||||
Amount: amount,
|
||||
TxID: l.TxHash,
|
||||
BlockHeight: blockNum,
|
||||
})
|
||||
}
|
||||
return events, nil
|
||||
}
|
||||
|
||||
func (o *OPNETPlugin) rpcCall(ctx context.Context, method string, params interface{}) (json.RawMessage, error) {
|
||||
reqBody := map[string]interface{}{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": method,
|
||||
"params": params,
|
||||
}
|
||||
body, err := json.Marshal(reqBody)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, o.rpcURL, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := o.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var rpcResp struct {
|
||||
Result json.RawMessage `json:"result"`
|
||||
Error *struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
} `json:"error"`
|
||||
}
|
||||
if err := json.Unmarshal(respBody, &rpcResp); err != nil {
|
||||
return nil, fmt.Errorf("rpc decode: %w", err)
|
||||
}
|
||||
if rpcResp.Error != nil {
|
||||
return nil, fmt.Errorf("rpc error %d: %s", rpcResp.Error.Code, rpcResp.Error.Message)
|
||||
}
|
||||
return rpcResp.Result, nil
|
||||
}
|
||||
|
||||
func hexToBytes(s string) ([]byte, error) {
|
||||
if len(s) >= 2 && s[:2] == "0x" {
|
||||
s = s[2:]
|
||||
}
|
||||
if len(s)%2 != 0 {
|
||||
s = "0" + s
|
||||
}
|
||||
b := make([]byte, len(s)/2)
|
||||
for i := 0; i < len(b); i++ {
|
||||
hi := unhex(s[2*i])
|
||||
lo := unhex(s[2*i+1])
|
||||
if hi == 0xff || lo == 0xff {
|
||||
return nil, fmt.Errorf("invalid hex at %d", 2*i)
|
||||
}
|
||||
b[i] = hi<<4 | lo
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func unhex(c byte) byte {
|
||||
switch {
|
||||
case '0' <= c && c <= '9':
|
||||
return c - '0'
|
||||
case 'a' <= c && c <= 'f':
|
||||
return c - 'a' + 10
|
||||
case 'A' <= c && c <= 'F':
|
||||
return c - 'A' + 10
|
||||
default:
|
||||
return 0xff
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/big"
|
||||
)
|
||||
|
||||
// DepositEvent is the canonical deposit event format emitted by all source-chain adapters.
|
||||
// Maps 1:1 to Teleporter.mintDeposit parameters on Lux C-Chain.
|
||||
type DepositEvent struct {
|
||||
SrcChainID uint64 // Source chain identifier (OPNET=4294967299, SOLANA=1399811149, TON=1414483791)
|
||||
Nonce uint64 // Monotonic deposit nonce on source chain
|
||||
Recipient [20]byte // Lux C-Chain recipient address (EVM)
|
||||
Amount *big.Int // Deposit amount in source chain native units
|
||||
TxID string // Source chain transaction identifier
|
||||
BlockHeight uint64 // Source chain block/slot height
|
||||
}
|
||||
|
||||
// ChainPlugin abstracts a source-chain block indexer for the Teleport bridge watcher.
|
||||
// Each implementation polls its chain for deposit events and converts them to the
|
||||
// canonical format consumed by the signer and relay pipeline.
|
||||
type ChainPlugin interface {
|
||||
// Name returns a human-readable identifier (e.g., "opnet", "solana", "ton").
|
||||
Name() string
|
||||
|
||||
// ChainID returns the Teleporter chain identifier for this source chain.
|
||||
ChainID() uint64
|
||||
|
||||
// PollDeposits fetches deposit events from fromSlot onward.
|
||||
// Returns events found and the latest processed slot/block height.
|
||||
// The watcher calls this on each tick interval.
|
||||
PollDeposits(ctx context.Context, fromSlot uint64) ([]DepositEvent, uint64, error)
|
||||
|
||||
// QueryBacking returns the total value locked in the bridge contract on this chain.
|
||||
// Used for periodic backing attestations to Teleporter.updateBacking.
|
||||
QueryBacking(ctx context.Context) (*big.Int, error)
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
const PolkadotChainID uint64 = 0x444F5400 // "DOT\x00" in Teleporter namespace
|
||||
|
||||
// PolkadotPlugin polls a Substrate JSON-RPC node for deposit extrinsics
|
||||
// from the bridge pallet. Polkadot uses sr25519 signing.
|
||||
type PolkadotPlugin struct {
|
||||
rpcURL string
|
||||
bridgeAddr string // SS58 address
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func NewPolkadotPlugin(rpcURL, bridgeAddr string) *PolkadotPlugin {
|
||||
return &PolkadotPlugin{
|
||||
rpcURL: rpcURL,
|
||||
bridgeAddr: bridgeAddr,
|
||||
client: &http.Client{Timeout: 15 * time.Second},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PolkadotPlugin) Name() string { return "polkadot" }
|
||||
func (p *PolkadotPlugin) ChainID() uint64 { return PolkadotChainID }
|
||||
|
||||
func (p *PolkadotPlugin) PollDeposits(ctx context.Context, fromBlock uint64) ([]DepositEvent, uint64, error) {
|
||||
return nil, fromBlock, fmt.Errorf("polkadot: not yet implemented")
|
||||
}
|
||||
|
||||
func (p *PolkadotPlugin) QueryBacking(ctx context.Context) (*big.Int, error) {
|
||||
return nil, fmt.Errorf("polkadot: not yet implemented")
|
||||
}
|
||||
@@ -0,0 +1,380 @@
|
||||
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
const SolanaChainID uint64 = 1399811149 // "SOLN" in Teleporter namespace
|
||||
|
||||
// SolanaPlugin polls a Solana RPC node for LockEvent logs from the Lux bridge program
|
||||
// and converts them to canonical DepositEvent format.
|
||||
type SolanaPlugin struct {
|
||||
rpcURL string
|
||||
programID string // Base58 bridge program address
|
||||
vaultAddress string // Base58 bridge config PDA (precomputed, not derived at runtime)
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func NewSolanaPlugin(rpcURL, programID, vaultAddress string) *SolanaPlugin {
|
||||
return &SolanaPlugin{
|
||||
rpcURL: rpcURL,
|
||||
programID: programID,
|
||||
vaultAddress: vaultAddress,
|
||||
client: &http.Client{Timeout: 15 * time.Second},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SolanaPlugin) Name() string { return "solana" }
|
||||
func (s *SolanaPlugin) ChainID() uint64 { return SolanaChainID }
|
||||
|
||||
// PollDeposits fetches confirmed transaction signatures for the bridge program
|
||||
// starting from the given slot, then parses LockEvent data from each transaction's logs.
|
||||
func (s *SolanaPlugin) PollDeposits(ctx context.Context, fromSlot uint64) ([]DepositEvent, uint64, error) {
|
||||
// Get current slot to bound the search.
|
||||
currentSlot, err := s.getSlot(ctx)
|
||||
if err != nil {
|
||||
return nil, fromSlot, fmt.Errorf("solana: get slot: %w", err)
|
||||
}
|
||||
if currentSlot <= fromSlot {
|
||||
return nil, fromSlot, nil
|
||||
}
|
||||
|
||||
// Fetch signatures for the bridge program in the slot range.
|
||||
sigs, err := s.getSignaturesForAddress(ctx, fromSlot, currentSlot)
|
||||
if err != nil {
|
||||
return nil, fromSlot, fmt.Errorf("solana: get signatures: %w", err)
|
||||
}
|
||||
|
||||
var events []DepositEvent
|
||||
for _, sig := range sigs {
|
||||
tx, err := s.getTransaction(ctx, sig.Signature)
|
||||
if err != nil {
|
||||
return events, sig.Slot, fmt.Errorf("solana: get tx %s: %w", sig.Signature, err)
|
||||
}
|
||||
|
||||
deposit, ok := parseLockEventFromLogs(tx.Meta.LogMessages, sig.Slot, sig.Signature)
|
||||
if ok {
|
||||
events = append(events, deposit)
|
||||
}
|
||||
}
|
||||
|
||||
return events, currentSlot, nil
|
||||
}
|
||||
|
||||
// QueryBacking calls the bridge program's total_locked account data via getAccountInfo.
|
||||
func (s *SolanaPlugin) QueryBacking(ctx context.Context) (*big.Int, error) {
|
||||
// The BridgeConfig PDA stores total locked value.
|
||||
// PDA seeds: ["bridge_config"] — derived deterministically from the program ID.
|
||||
// For now we read the account and parse the total_locked field from its data.
|
||||
configPDA, err := s.deriveBridgeConfigPDA()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("solana: derive config PDA: %w", err)
|
||||
}
|
||||
|
||||
info, err := s.getAccountInfo(ctx, configPDA)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("solana: get account info: %w", err)
|
||||
}
|
||||
|
||||
data, err := base64.StdEncoding.DecodeString(info.Data[0])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("solana: decode account data: %w", err)
|
||||
}
|
||||
|
||||
// BridgeConfig layout (after 8-byte discriminator):
|
||||
// admin: 32 bytes (offset 8)
|
||||
// mpc_signers: 96 bytes (offset 40)
|
||||
// threshold: 1 byte (offset 136)
|
||||
// fee_bps: 2 bytes (offset 137)
|
||||
// fee_collector: 32 bytes (offset 139)
|
||||
// paused: 1 byte (offset 171)
|
||||
// outbound_nonce: 8 bytes (offset 172)
|
||||
// chain_id: 8 bytes (offset 180)
|
||||
// bump: 1 byte (offset 188)
|
||||
//
|
||||
// Total locked is tracked in the token vault accounts, not in BridgeConfig.
|
||||
// Sum vault token balances via getTokenAccountBalance for each registered vault.
|
||||
//
|
||||
// For the backing attestation, we query vault balance directly.
|
||||
_ = data
|
||||
return s.queryVaultBalance(ctx)
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────
|
||||
// Solana RPC helpers
|
||||
// ────────────────────────────────────────────────────────────────────
|
||||
|
||||
type solanaSignature struct {
|
||||
Signature string `json:"signature"`
|
||||
Slot uint64 `json:"slot"`
|
||||
}
|
||||
|
||||
type solanaTx struct {
|
||||
Meta struct {
|
||||
LogMessages []string `json:"logMessages"`
|
||||
} `json:"meta"`
|
||||
}
|
||||
|
||||
type solanaAccountInfo struct {
|
||||
Data []string `json:"data"` // [base64_data, encoding]
|
||||
}
|
||||
|
||||
func (s *SolanaPlugin) getSlot(ctx context.Context) (uint64, error) {
|
||||
result, err := s.rpcCall(ctx, "getSlot", []interface{}{
|
||||
map[string]string{"commitment": "confirmed"},
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
var slot uint64
|
||||
if err := json.Unmarshal(result, &slot); err != nil {
|
||||
return 0, fmt.Errorf("parse slot: %w", err)
|
||||
}
|
||||
return slot, nil
|
||||
}
|
||||
|
||||
func (s *SolanaPlugin) getSignaturesForAddress(ctx context.Context, minSlot, maxSlot uint64) ([]solanaSignature, error) {
|
||||
params := []interface{}{
|
||||
s.programID,
|
||||
map[string]interface{}{
|
||||
"commitment": "confirmed",
|
||||
"minContext": map[string]uint64{"slot": minSlot},
|
||||
},
|
||||
}
|
||||
result, err := s.rpcCall(ctx, "getSignaturesForAddress", params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var sigs []solanaSignature
|
||||
if err := json.Unmarshal(result, &sigs); err != nil {
|
||||
return nil, fmt.Errorf("parse signatures: %w", err)
|
||||
}
|
||||
// Filter to slot range.
|
||||
var filtered []solanaSignature
|
||||
for _, sig := range sigs {
|
||||
if sig.Slot > minSlot && sig.Slot <= maxSlot {
|
||||
filtered = append(filtered, sig)
|
||||
}
|
||||
}
|
||||
return filtered, nil
|
||||
}
|
||||
|
||||
func (s *SolanaPlugin) getTransaction(ctx context.Context, sig string) (*solanaTx, error) {
|
||||
params := []interface{}{
|
||||
sig,
|
||||
map[string]interface{}{
|
||||
"encoding": "json",
|
||||
"commitment": "confirmed",
|
||||
"maxSupportedTransactionVersion": 0,
|
||||
},
|
||||
}
|
||||
result, err := s.rpcCall(ctx, "getTransaction", params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var tx solanaTx
|
||||
if err := json.Unmarshal(result, &tx); err != nil {
|
||||
return nil, fmt.Errorf("parse tx: %w", err)
|
||||
}
|
||||
return &tx, nil
|
||||
}
|
||||
|
||||
func (s *SolanaPlugin) getAccountInfo(ctx context.Context, address string) (*solanaAccountInfo, error) {
|
||||
params := []interface{}{
|
||||
address,
|
||||
map[string]string{"encoding": "base64", "commitment": "confirmed"},
|
||||
}
|
||||
result, err := s.rpcCall(ctx, "getAccountInfo", params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var wrapper struct {
|
||||
Value *solanaAccountInfo `json:"value"`
|
||||
}
|
||||
if err := json.Unmarshal(result, &wrapper); err != nil {
|
||||
return nil, fmt.Errorf("parse account info: %w", err)
|
||||
}
|
||||
if wrapper.Value == nil {
|
||||
return nil, fmt.Errorf("account not found: %s", address)
|
||||
}
|
||||
return wrapper.Value, nil
|
||||
}
|
||||
|
||||
func (s *SolanaPlugin) queryVaultBalance(ctx context.Context) (*big.Int, error) {
|
||||
// Query program token accounts owned by the vault PDA.
|
||||
// getTokenAccountsByOwner returns all SPL token accounts.
|
||||
// Sum their balances for total backing.
|
||||
params := []interface{}{
|
||||
s.programID,
|
||||
map[string]string{"programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"},
|
||||
map[string]string{"encoding": "jsonParsed"},
|
||||
}
|
||||
result, err := s.rpcCall(ctx, "getTokenAccountsByOwner", params)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get token accounts: %w", err)
|
||||
}
|
||||
|
||||
var wrapper struct {
|
||||
Value []struct {
|
||||
Account struct {
|
||||
Data struct {
|
||||
Parsed struct {
|
||||
Info struct {
|
||||
TokenAmount struct {
|
||||
Amount string `json:"amount"`
|
||||
} `json:"tokenAmount"`
|
||||
} `json:"info"`
|
||||
} `json:"parsed"`
|
||||
} `json:"data"`
|
||||
} `json:"account"`
|
||||
} `json:"value"`
|
||||
}
|
||||
if err := json.Unmarshal(result, &wrapper); err != nil {
|
||||
return nil, fmt.Errorf("parse token accounts: %w", err)
|
||||
}
|
||||
|
||||
total := new(big.Int)
|
||||
for _, acct := range wrapper.Value {
|
||||
bal, ok := new(big.Int).SetString(acct.Account.Data.Parsed.Info.TokenAmount.Amount, 10)
|
||||
if ok {
|
||||
total.Add(total, bal)
|
||||
}
|
||||
}
|
||||
return total, nil
|
||||
}
|
||||
|
||||
func (s *SolanaPlugin) deriveBridgeConfigPDA() (string, error) {
|
||||
if s.vaultAddress == "" {
|
||||
return "", fmt.Errorf("solana: vaultAddress not configured (set to precomputed findProgramAddress([\"bridge_config\"], programID))")
|
||||
}
|
||||
return s.vaultAddress, nil
|
||||
}
|
||||
|
||||
// parseLockEventFromLogs extracts a DepositEvent from Anchor program log messages.
|
||||
// Anchor emits events as base64-encoded borsh in "Program data: <base64>" log lines.
|
||||
func parseLockEventFromLogs(logs []string, slot uint64, txSig string) (DepositEvent, bool) {
|
||||
// Anchor event discriminator for LockEvent: SHA256("event:LockEvent")[:8]
|
||||
lockDiscriminator := []byte{0x65, 0xc4, 0x2b, 0x6f, 0x39, 0x48, 0x3e, 0x09}
|
||||
|
||||
for _, line := range logs {
|
||||
if len(line) < 15 {
|
||||
continue
|
||||
}
|
||||
// Anchor logs: "Program data: <base64>"
|
||||
const prefix = "Program data: "
|
||||
idx := 0
|
||||
for i := 0; i <= len(line)-len(prefix); i++ {
|
||||
if line[i:i+len(prefix)] == prefix {
|
||||
idx = i + len(prefix)
|
||||
break
|
||||
}
|
||||
}
|
||||
if idx == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
data, err := base64.StdEncoding.DecodeString(line[idx:])
|
||||
if err != nil || len(data) < 8 {
|
||||
continue
|
||||
}
|
||||
|
||||
if !bytes.Equal(data[:8], lockDiscriminator) {
|
||||
continue
|
||||
}
|
||||
|
||||
// LockEvent borsh layout (after 8-byte discriminator):
|
||||
// source_chain: u64 (8 bytes)
|
||||
// dest_chain: u64 (8 bytes)
|
||||
// nonce: u64 (8 bytes)
|
||||
// token: [32]u8
|
||||
// sender: [32]u8
|
||||
// recipient: [32]u8 (EVM address zero-padded to 32 bytes)
|
||||
// amount: u64 (8 bytes)
|
||||
// fee: u64 (8 bytes)
|
||||
// timestamp: i64 (8 bytes)
|
||||
const eventLen = 8 + 8 + 8 + 32 + 32 + 32 + 8 + 8 + 8 // 144
|
||||
if len(data) < 8+eventLen {
|
||||
continue
|
||||
}
|
||||
d := data[8:]
|
||||
|
||||
nonce := le64(d[16:24])
|
||||
amount := new(big.Int).SetUint64(le64(d[112:120]))
|
||||
|
||||
var recipient [20]byte
|
||||
// EVM address is in the last 20 bytes of the 32-byte recipient field at offset 88
|
||||
copy(recipient[:], d[100:120]) // bytes 12-31 of the 32-byte field at offset 88
|
||||
|
||||
return DepositEvent{
|
||||
SrcChainID: SolanaChainID,
|
||||
Nonce: nonce,
|
||||
Recipient: recipient,
|
||||
Amount: amount,
|
||||
TxID: txSig,
|
||||
BlockHeight: slot,
|
||||
}, true
|
||||
}
|
||||
return DepositEvent{}, false
|
||||
}
|
||||
|
||||
func le64(b []byte) uint64 {
|
||||
_ = b[7]
|
||||
return uint64(b[0]) | uint64(b[1])<<8 | uint64(b[2])<<16 | uint64(b[3])<<24 |
|
||||
uint64(b[4])<<32 | uint64(b[5])<<40 | uint64(b[6])<<48 | uint64(b[7])<<56
|
||||
}
|
||||
|
||||
func (s *SolanaPlugin) rpcCall(ctx context.Context, method string, params interface{}) (json.RawMessage, error) {
|
||||
reqBody := map[string]interface{}{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": method,
|
||||
"params": params,
|
||||
}
|
||||
body, err := json.Marshal(reqBody)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, s.rpcURL, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := s.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var rpcResp struct {
|
||||
Result json.RawMessage `json:"result"`
|
||||
Error *struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
} `json:"error"`
|
||||
}
|
||||
if err := json.Unmarshal(respBody, &rpcResp); err != nil {
|
||||
return nil, fmt.Errorf("rpc decode: %w", err)
|
||||
}
|
||||
if rpcResp.Error != nil {
|
||||
return nil, fmt.Errorf("rpc error %d: %s", rpcResp.Error.Code, rpcResp.Error.Message)
|
||||
}
|
||||
return rpcResp.Result, nil
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
const StacksChainID uint64 = 4294967440
|
||||
|
||||
// StacksPlugin polls a Stacks node API for deposit transactions from the
|
||||
// bridge Clarity contract. Stacks uses secp256k1 signing (Bitcoin-derived).
|
||||
type StacksPlugin struct {
|
||||
apiURL string
|
||||
bridgeAddr string // Clarity contract principal (SP...)
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func NewStacksPlugin(apiURL, bridgeAddr string) *StacksPlugin {
|
||||
return &StacksPlugin{
|
||||
apiURL: apiURL,
|
||||
bridgeAddr: bridgeAddr,
|
||||
client: &http.Client{Timeout: 15 * time.Second},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *StacksPlugin) Name() string { return "stacks" }
|
||||
func (s *StacksPlugin) ChainID() uint64 { return StacksChainID }
|
||||
|
||||
func (s *StacksPlugin) PollDeposits(ctx context.Context, fromBlock uint64) ([]DepositEvent, uint64, error) {
|
||||
return nil, fromBlock, fmt.Errorf("stacks: not yet implemented")
|
||||
}
|
||||
|
||||
func (s *StacksPlugin) QueryBacking(ctx context.Context) (*big.Int, error) {
|
||||
return nil, fmt.Errorf("stacks: not yet implemented")
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
const StarkNetChainID uint64 = 0x53544B00 // "STK\x00" in Teleporter namespace
|
||||
|
||||
// StarkNetPlugin polls a StarkNet JSON-RPC node for deposit events from the
|
||||
// bridge contract. StarkNet uses the Stark curve for signing.
|
||||
type StarkNetPlugin struct {
|
||||
rpcURL string
|
||||
bridgeAddr string // StarkNet contract address (0x-prefixed felt)
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func NewStarkNetPlugin(rpcURL, bridgeAddr string) *StarkNetPlugin {
|
||||
return &StarkNetPlugin{
|
||||
rpcURL: rpcURL,
|
||||
bridgeAddr: bridgeAddr,
|
||||
client: &http.Client{Timeout: 15 * time.Second},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *StarkNetPlugin) Name() string { return "starknet" }
|
||||
func (s *StarkNetPlugin) ChainID() uint64 { return StarkNetChainID }
|
||||
|
||||
func (s *StarkNetPlugin) PollDeposits(ctx context.Context, fromBlock uint64) ([]DepositEvent, uint64, error) {
|
||||
return nil, fromBlock, fmt.Errorf("starknet: not yet implemented")
|
||||
}
|
||||
|
||||
func (s *StarkNetPlugin) QueryBacking(ctx context.Context) (*big.Int, error) {
|
||||
return nil, fmt.Errorf("starknet: not yet implemented")
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
const StellarChainID uint64 = 0x584C4D00 // "XLM\x00" in Teleporter namespace
|
||||
|
||||
// StellarPlugin polls the Stellar Horizon API for deposit operations
|
||||
// from the bridge account. Stellar uses ed25519 signing.
|
||||
type StellarPlugin struct {
|
||||
horizonURL string
|
||||
bridgeAddr string // Stellar public key (G...)
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func NewStellarPlugin(horizonURL, bridgeAddr string) *StellarPlugin {
|
||||
return &StellarPlugin{
|
||||
horizonURL: horizonURL,
|
||||
bridgeAddr: bridgeAddr,
|
||||
client: &http.Client{Timeout: 15 * time.Second},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *StellarPlugin) Name() string { return "stellar" }
|
||||
func (s *StellarPlugin) ChainID() uint64 { return StellarChainID }
|
||||
|
||||
func (s *StellarPlugin) PollDeposits(ctx context.Context, fromLedger uint64) ([]DepositEvent, uint64, error) {
|
||||
return nil, fromLedger, fmt.Errorf("stellar: not yet implemented")
|
||||
}
|
||||
|
||||
func (s *StellarPlugin) QueryBacking(ctx context.Context) (*big.Int, error) {
|
||||
return nil, fmt.Errorf("stellar: not yet implemented")
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
const SuiChainID uint64 = 0x53554900 // "SUI\x00" in Teleporter namespace
|
||||
|
||||
// SuiPlugin polls a Sui JSON-RPC node for deposit events from the bridge Move module.
|
||||
// Sui uses ed25519 signing and object-centric state.
|
||||
type SuiPlugin struct {
|
||||
rpcURL string
|
||||
bridgeAddr string // Move module object ID
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func NewSuiPlugin(rpcURL, bridgeAddr string) *SuiPlugin {
|
||||
return &SuiPlugin{
|
||||
rpcURL: rpcURL,
|
||||
bridgeAddr: bridgeAddr,
|
||||
client: &http.Client{Timeout: 15 * time.Second},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SuiPlugin) Name() string { return "sui" }
|
||||
func (s *SuiPlugin) ChainID() uint64 { return SuiChainID }
|
||||
|
||||
func (s *SuiPlugin) PollDeposits(ctx context.Context, fromCheckpoint uint64) ([]DepositEvent, uint64, error) {
|
||||
return nil, fromCheckpoint, fmt.Errorf("sui: not yet implemented")
|
||||
}
|
||||
|
||||
func (s *SuiPlugin) QueryBacking(ctx context.Context) (*big.Int, error) {
|
||||
return nil, fmt.Errorf("sui: not yet implemented")
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
const TezosChainID uint64 = 4294967500
|
||||
|
||||
// TezosPlugin polls a Tezos RPC node for deposit operations from the bridge
|
||||
// Michelson contract. Tezos uses ed25519 signing.
|
||||
type TezosPlugin struct {
|
||||
rpcURL string
|
||||
bridgeAddr string // KT1... contract address
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func NewTezosPlugin(rpcURL, bridgeAddr string) *TezosPlugin {
|
||||
return &TezosPlugin{
|
||||
rpcURL: rpcURL,
|
||||
bridgeAddr: bridgeAddr,
|
||||
client: &http.Client{Timeout: 15 * time.Second},
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TezosPlugin) Name() string { return "tezos" }
|
||||
func (t *TezosPlugin) ChainID() uint64 { return TezosChainID }
|
||||
|
||||
func (t *TezosPlugin) PollDeposits(ctx context.Context, fromLevel uint64) ([]DepositEvent, uint64, error) {
|
||||
return nil, fromLevel, fmt.Errorf("tezos: not yet implemented")
|
||||
}
|
||||
|
||||
func (t *TezosPlugin) QueryBacking(ctx context.Context) (*big.Int, error) {
|
||||
return nil, fmt.Errorf("tezos: not yet implemented")
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
const TONChainID uint64 = 1414483791 // "TONO" in Teleporter namespace
|
||||
|
||||
// TONPlugin polls a TON HTTP API (toncenter/liteserver) for deposit external messages
|
||||
// from the TeleportVault contract and converts them to canonical DepositEvent format.
|
||||
type TONPlugin struct {
|
||||
apiURL string // TON HTTP API base URL (e.g., https://toncenter.com/api/v2)
|
||||
contract string // TeleportVault contract address (base64url or raw)
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func NewTONPlugin(apiURL, contract string) *TONPlugin {
|
||||
return &TONPlugin{
|
||||
apiURL: apiURL,
|
||||
contract: contract,
|
||||
client: &http.Client{Timeout: 15 * time.Second},
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TONPlugin) Name() string { return "ton" }
|
||||
func (t *TONPlugin) ChainID() uint64 { return TONChainID }
|
||||
|
||||
// PollDeposits fetches transactions to the bridge contract since the given logical time,
|
||||
// filters for deposit external out messages, and converts to DepositEvent.
|
||||
// TON uses logical time (lt) instead of block height for ordering.
|
||||
func (t *TONPlugin) PollDeposits(ctx context.Context, fromLT uint64) ([]DepositEvent, uint64, error) {
|
||||
txs, err := t.getTransactions(ctx, fromLT)
|
||||
if err != nil {
|
||||
return nil, fromLT, fmt.Errorf("ton: get transactions: %w", err)
|
||||
}
|
||||
|
||||
var events []DepositEvent
|
||||
var maxLT uint64 = fromLT
|
||||
|
||||
for _, tx := range txs {
|
||||
if tx.LT <= fromLT {
|
||||
continue
|
||||
}
|
||||
if tx.LT > maxLT {
|
||||
maxLT = tx.LT
|
||||
}
|
||||
|
||||
// Parse deposit events from external out messages.
|
||||
for _, msg := range tx.OutMsgs {
|
||||
if msg.MsgType != "ext_out_msg" {
|
||||
continue
|
||||
}
|
||||
deposit, ok := parseDepositFromBOC(msg.Body, tx.LT, tx.Hash)
|
||||
if ok {
|
||||
events = append(events, deposit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return events, maxLT, nil
|
||||
}
|
||||
|
||||
// QueryBacking calls the get_total_locked get-method on the TeleportVault contract.
|
||||
func (t *TONPlugin) QueryBacking(ctx context.Context) (*big.Int, error) {
|
||||
result, err := t.runGetMethod(ctx, "get_total_locked")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ton: query backing: %w", err)
|
||||
}
|
||||
|
||||
if len(result.Stack) == 0 {
|
||||
return nil, fmt.Errorf("ton: get_total_locked returned empty stack")
|
||||
}
|
||||
|
||||
// TVM returns integers as ["num", "0x..."] tuples.
|
||||
val, ok := new(big.Int).SetString(result.Stack[0].Value, 0)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("ton: parse total_locked: %s", result.Stack[0].Value)
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────
|
||||
// TON HTTP API types and helpers
|
||||
// ────────────────────────────────────────────────────────────────────
|
||||
|
||||
type tonTransaction struct {
|
||||
LT uint64 `json:"lt"`
|
||||
Hash string `json:"hash"`
|
||||
OutMsgs []tonMessage `json:"out_msgs"`
|
||||
}
|
||||
|
||||
type tonMessage struct {
|
||||
MsgType string `json:"msg_type"`
|
||||
Body string `json:"body"` // base64-encoded BOC
|
||||
}
|
||||
|
||||
type tonGetMethodResult struct {
|
||||
Stack []tonStackEntry `json:"stack"`
|
||||
}
|
||||
|
||||
type tonStackEntry struct {
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
func (t *TONPlugin) getTransactions(ctx context.Context, afterLT uint64) ([]tonTransaction, error) {
|
||||
url := fmt.Sprintf("%s/getTransactions?address=%s&limit=100<=%d&archival=true",
|
||||
t.apiURL, t.contract, afterLT)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := t.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var apiResp struct {
|
||||
OK bool `json:"ok"`
|
||||
Result []tonTransaction `json:"result"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &apiResp); err != nil {
|
||||
return nil, fmt.Errorf("parse response: %w", err)
|
||||
}
|
||||
if !apiResp.OK {
|
||||
return nil, fmt.Errorf("api error: %s", apiResp.Error)
|
||||
}
|
||||
return apiResp.Result, nil
|
||||
}
|
||||
|
||||
func (t *TONPlugin) runGetMethod(ctx context.Context, method string) (*tonGetMethodResult, error) {
|
||||
url := fmt.Sprintf("%s/runGetMethod?address=%s&method=%s",
|
||||
t.apiURL, t.contract, method)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := t.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var apiResp struct {
|
||||
OK bool `json:"ok"`
|
||||
Result tonGetMethodResult `json:"result"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &apiResp); err != nil {
|
||||
return nil, fmt.Errorf("parse response: %w", err)
|
||||
}
|
||||
if !apiResp.OK {
|
||||
return nil, fmt.Errorf("api error: %s", apiResp.Error)
|
||||
}
|
||||
return &apiResp.Result, nil
|
||||
}
|
||||
|
||||
// parseDepositFromBOC extracts a DepositEvent from a TON external out message body.
|
||||
// The TeleportVault emits deposit events as external messages with the layout:
|
||||
//
|
||||
// op(32) chain_id(64) nonce(64) lux_recipient(256) amount(coins) timestamp(32)
|
||||
//
|
||||
// The body is base64-encoded BOC (Bag of Cells).
|
||||
func parseDepositFromBOC(bodyB64 string, lt uint64, txHash string) (DepositEvent, bool) {
|
||||
data, err := base64.StdEncoding.DecodeString(bodyB64)
|
||||
if err != nil {
|
||||
return DepositEvent{}, false
|
||||
}
|
||||
|
||||
// Minimal BOC parsing: skip BOC envelope to get to the cell data.
|
||||
// TON BOC format: magic(4) + flags(1) + cells(1) + roots(1) + absent(1) + size(1) + ...
|
||||
// For single-cell BOCs from external out messages, the cell data starts after the header.
|
||||
// The exact offset depends on the BOC serialization variant.
|
||||
//
|
||||
// We look for the deposit op tag (0x44455031) in the data.
|
||||
const depositOp uint32 = 0x44455031
|
||||
|
||||
cellData := findCellData(data)
|
||||
if cellData == nil || len(cellData) < 54 { // minimum: 4+8+8+32+2 bytes
|
||||
return DepositEvent{}, false
|
||||
}
|
||||
|
||||
// Verify op tag
|
||||
op := be32(cellData[0:4])
|
||||
if op != depositOp {
|
||||
return DepositEvent{}, false
|
||||
}
|
||||
|
||||
chainID := be64(cellData[4:12])
|
||||
nonce := be64(cellData[12:20])
|
||||
|
||||
// lux_recipient is 256 bits (32 bytes), EVM address in last 20 bytes
|
||||
var recipient [20]byte
|
||||
copy(recipient[:], cellData[32:52]) // bytes 12-31 of the 32-byte field at offset 20
|
||||
|
||||
// WARNING: Simplified parser. TON coins use VarUInteger16 encoding: first 4 bits = byte
|
||||
// count N, next N bytes = big-endian amount. This code assumes a fixed 8-byte big-endian
|
||||
// value at offset 52, which is only correct when the coin amount occupies exactly 8 bytes
|
||||
// (length nibble = 0x8). For production TON support, implement full VarUInteger16
|
||||
// deserialization (read 4-bit length prefix, then N bytes) or use tonutils-go.
|
||||
amount := new(big.Int)
|
||||
if len(cellData) >= 60 {
|
||||
amount.SetBytes(cellData[52:60])
|
||||
}
|
||||
|
||||
_ = chainID // already known (TONChainID)
|
||||
|
||||
return DepositEvent{
|
||||
SrcChainID: TONChainID,
|
||||
Nonce: nonce,
|
||||
Recipient: recipient,
|
||||
Amount: amount,
|
||||
TxID: txHash,
|
||||
BlockHeight: lt,
|
||||
}, true
|
||||
}
|
||||
|
||||
// findCellData locates the cell payload within a BOC envelope.
|
||||
// Handles the common single-cell BOC format.
|
||||
func findCellData(boc []byte) []byte {
|
||||
if len(boc) < 10 {
|
||||
return nil
|
||||
}
|
||||
// Standard BOC magic: b5ee9c72
|
||||
if boc[0] == 0xb5 && boc[1] == 0xee && boc[2] == 0x9c && boc[3] == 0x72 {
|
||||
// flags(1) size(1) cells(1) roots(1) absent(1) totCellsSize(size bytes) ...
|
||||
flags := boc[4]
|
||||
sizeBytes := int(flags & 0x07)
|
||||
if len(boc) < 5+sizeBytes*3+2 {
|
||||
return nil
|
||||
}
|
||||
// Skip header to cell data. For single-cell BOCs this is straightforward.
|
||||
headerSize := 4 + 1 + 1 + 1 + 1 + sizeBytes + sizeBytes
|
||||
if headerSize < len(boc) {
|
||||
return boc[headerSize:]
|
||||
}
|
||||
}
|
||||
// Fallback: return raw data (for non-standard encoding)
|
||||
return boc
|
||||
}
|
||||
|
||||
func be32(b []byte) uint32 {
|
||||
return uint32(b[0])<<24 | uint32(b[1])<<16 | uint32(b[2])<<8 | uint32(b[3])
|
||||
}
|
||||
|
||||
func be64(b []byte) uint64 {
|
||||
return uint64(b[0])<<56 | uint64(b[1])<<48 | uint64(b[2])<<40 | uint64(b[3])<<32 |
|
||||
uint64(b[4])<<24 | uint64(b[5])<<16 | uint64(b[6])<<8 | uint64(b[7])
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
const XRPLChainID uint64 = 0x5852504C // "XRPL" in Teleporter namespace
|
||||
|
||||
// XRPLPlugin polls an XRPL WebSocket/JSON-RPC node for deposit transactions
|
||||
// from the bridge account. XRPL supports secp256k1 and ed25519 signing.
|
||||
type XRPLPlugin struct {
|
||||
rpcURL string
|
||||
bridgeAddr string // XRPL classic address (r...)
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func NewXRPLPlugin(rpcURL, bridgeAddr string) *XRPLPlugin {
|
||||
return &XRPLPlugin{
|
||||
rpcURL: rpcURL,
|
||||
bridgeAddr: bridgeAddr,
|
||||
client: &http.Client{Timeout: 15 * time.Second},
|
||||
}
|
||||
}
|
||||
|
||||
func (x *XRPLPlugin) Name() string { return "xrpl" }
|
||||
func (x *XRPLPlugin) ChainID() uint64 { return XRPLChainID }
|
||||
|
||||
func (x *XRPLPlugin) PollDeposits(ctx context.Context, fromLedger uint64) ([]DepositEvent, uint64, error) {
|
||||
return nil, fromLedger, fmt.Errorf("xrpl: not yet implemented")
|
||||
}
|
||||
|
||||
func (x *XRPLPlugin) QueryBacking(ctx context.Context) (*big.Int, error) {
|
||||
return nil, fmt.Errorf("xrpl: not yet implemented")
|
||||
}
|
||||
@@ -0,0 +1,316 @@
|
||||
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/luxfi/crypto"
|
||||
)
|
||||
|
||||
// Relay submits signed proofs to the Teleporter contract on Lux C-Chain.
|
||||
type Relay struct {
|
||||
rpcURL string
|
||||
teleporterAddr string
|
||||
signer *Signer
|
||||
txSigner *Signer // NEW-02: separate hot wallet for tx submission in threshold mode
|
||||
httpClient *http.Client
|
||||
mu sync.Mutex // protects nonce
|
||||
nonce uint64
|
||||
chainID uint64
|
||||
}
|
||||
|
||||
// NewRelay creates a relay targeting the given Lux C-Chain RPC and Teleporter address.
|
||||
// txSigner is the hot wallet used for transaction submission. In single-key mode,
|
||||
// pass the same signer for both. In threshold mode, txSigner is a separate hot wallet
|
||||
// (the MPC signs proofs, the hot wallet submits them on-chain).
|
||||
func NewRelay(rpcURL, teleporterAddr string, signer *Signer, txSigner *Signer, chainID uint64) *Relay {
|
||||
return &Relay{
|
||||
rpcURL: rpcURL,
|
||||
teleporterAddr: teleporterAddr,
|
||||
signer: signer,
|
||||
txSigner: txSigner,
|
||||
httpClient: &http.Client{Timeout: 30 * time.Second},
|
||||
chainID: chainID,
|
||||
}
|
||||
}
|
||||
|
||||
// SubmitDeposit calls Teleporter.mintDeposit(srcChainId, depositNonce, recipient, amount, signature).
|
||||
//
|
||||
// Function selector: keccak256("mintDeposit(uint256,uint256,address,uint256,bytes)")[:4]
|
||||
func (r *Relay) SubmitDeposit(ctx context.Context, srcChainID, nonce uint64, recipient [20]byte, amount *big.Int, sig []byte) (string, error) {
|
||||
// mintDeposit(uint256,uint256,address,uint256,bytes)
|
||||
selector := funcSelector("mintDeposit(uint256,uint256,address,uint256,bytes)")
|
||||
|
||||
var calldata []byte
|
||||
calldata = append(calldata, selector[:]...)
|
||||
calldata = append(calldata, uint256Bytes(new(big.Int).SetUint64(srcChainID))...) // srcChainId
|
||||
calldata = append(calldata, uint256Bytes(new(big.Int).SetUint64(nonce))...) // depositNonce
|
||||
calldata = append(calldata, leftPad(recipient[:], 32)...) // recipient (address)
|
||||
calldata = append(calldata, uint256Bytes(amount)...) // amount
|
||||
calldata = append(calldata, uint256Bytes(new(big.Int).SetUint64(160))...) // offset to bytes (5 * 32)
|
||||
calldata = append(calldata, uint256Bytes(new(big.Int).SetUint64(uint64(len(sig))))...) // length of sig
|
||||
calldata = append(calldata, rightPad(sig, 32)...) // signature data
|
||||
|
||||
return r.sendTx(ctx, calldata)
|
||||
}
|
||||
|
||||
// SubmitBacking calls Teleporter.updateBacking(srcChainId, totalBacking, timestamp, signature).
|
||||
// CRITICAL-01: timestamp is now a parameter (signed by MPC, validated on-chain).
|
||||
//
|
||||
// Function selector: keccak256("updateBacking(uint256,uint256,uint256,bytes)")[:4]
|
||||
func (r *Relay) SubmitBacking(ctx context.Context, srcChainID uint64, totalBacking *big.Int, timestamp uint64, sig []byte) (string, error) {
|
||||
selector := funcSelector("updateBacking(uint256,uint256,uint256,bytes)")
|
||||
|
||||
var calldata []byte
|
||||
calldata = append(calldata, selector[:]...)
|
||||
calldata = append(calldata, uint256Bytes(new(big.Int).SetUint64(srcChainID))...) // srcChainId
|
||||
calldata = append(calldata, uint256Bytes(totalBacking)...) // totalBacking
|
||||
calldata = append(calldata, uint256Bytes(new(big.Int).SetUint64(timestamp))...) // timestamp
|
||||
calldata = append(calldata, uint256Bytes(new(big.Int).SetUint64(128))...) // offset to bytes (4 * 32)
|
||||
calldata = append(calldata, uint256Bytes(new(big.Int).SetUint64(uint64(len(sig))))...) // length of sig
|
||||
calldata = append(calldata, rightPad(sig, 32)...) // signature data
|
||||
|
||||
return r.sendTx(ctx, calldata)
|
||||
}
|
||||
|
||||
// sendTx builds, signs locally, and submits via eth_sendRawTransaction.
|
||||
// CRITICAL-03: Never use eth_sendTransaction — it exposes the node key.
|
||||
// NEW-02: Uses txSigner (hot wallet) for transaction signing, not the MPC signer.
|
||||
func (r *Relay) sendTx(ctx context.Context, calldata []byte) (string, error) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
// Fetch nonce from chain if we haven't yet.
|
||||
if r.nonce == 0 {
|
||||
from := r.txSigner.Address()
|
||||
n, err := r.fetchNonce(ctx, from)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("relay fetch nonce: %w", err)
|
||||
}
|
||||
r.nonce = n
|
||||
}
|
||||
|
||||
// Fetch gas price.
|
||||
gasPrice, err := r.fetchGasPrice(ctx)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("relay fetch gas price: %w", err)
|
||||
}
|
||||
|
||||
// Build EIP-155 transaction and RLP-encode.
|
||||
rawTx := r.buildRawTx(calldata, r.nonce, gasPrice)
|
||||
|
||||
// Sign with hot wallet key (txSigner always has a local key).
|
||||
signedTx, err := r.txSigner.SignTransaction(rawTx, r.chainID)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("relay sign tx: %w", err)
|
||||
}
|
||||
|
||||
// Submit signed transaction.
|
||||
result, err := r.ethCall(ctx, "eth_sendRawTransaction", []interface{}{"0x" + hex.EncodeToString(signedTx)})
|
||||
if err != nil {
|
||||
// Nonce recovery: re-fetch from chain on next attempt.
|
||||
r.nonce = 0
|
||||
return "", fmt.Errorf("relay send raw tx: %w", err)
|
||||
}
|
||||
|
||||
r.nonce++
|
||||
|
||||
var txHash string
|
||||
if err := json.Unmarshal(result, &txHash); err != nil {
|
||||
return "", fmt.Errorf("relay parse tx hash: %w", err)
|
||||
}
|
||||
return txHash, nil
|
||||
}
|
||||
|
||||
// fetchNonce gets the current transaction count for the signer address.
|
||||
func (r *Relay) fetchNonce(ctx context.Context, addr [20]byte) (uint64, error) {
|
||||
result, err := r.ethCall(ctx, "eth_getTransactionCount", []interface{}{
|
||||
"0x" + hex.EncodeToString(addr[:]), "pending",
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
var hexNonce string
|
||||
if err := json.Unmarshal(result, &hexNonce); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
n := new(big.Int)
|
||||
n.SetString(hexNonce, 0)
|
||||
return n.Uint64(), nil
|
||||
}
|
||||
|
||||
// fetchGasPrice queries eth_gasPrice.
|
||||
func (r *Relay) fetchGasPrice(ctx context.Context) (*big.Int, error) {
|
||||
result, err := r.ethCall(ctx, "eth_gasPrice", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var hexPrice string
|
||||
if err := json.Unmarshal(result, &hexPrice); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p := new(big.Int)
|
||||
p.SetString(hexPrice, 0)
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// buildRawTx returns the unsigned EIP-155 transaction RLP payload.
|
||||
// Format: RLP([nonce, gasPrice, gasLimit, to, value, data, chainId, 0, 0])
|
||||
func (r *Relay) buildRawTx(calldata []byte, nonce uint64, gasPrice *big.Int) []byte {
|
||||
addrHex := r.teleporterAddr
|
||||
if len(addrHex) >= 2 && addrHex[:2] == "0x" {
|
||||
addrHex = addrHex[2:]
|
||||
}
|
||||
to, _ := hex.DecodeString(addrHex)
|
||||
gasLimit := uint64(1_000_000)
|
||||
|
||||
return rlpEncodeTx(nonce, gasPrice, gasLimit, to, big.NewInt(0), calldata, r.chainID)
|
||||
}
|
||||
|
||||
// rlpEncodeTx produces EIP-155 unsigned tx: RLP([nonce, gasPrice, gasLimit, to, value, data, chainId, 0, 0])
|
||||
func rlpEncodeTx(nonce uint64, gasPrice *big.Int, gasLimit uint64, to []byte, value *big.Int, data []byte, chainID uint64) []byte {
|
||||
items := [][]byte{
|
||||
rlpEncodeUint(nonce),
|
||||
rlpEncodeBigInt(gasPrice),
|
||||
rlpEncodeUint(gasLimit),
|
||||
rlpEncodeBytes(to),
|
||||
rlpEncodeBigInt(value),
|
||||
rlpEncodeBytes(data),
|
||||
rlpEncodeUint(chainID),
|
||||
rlpEncodeBytes(nil), // 0
|
||||
rlpEncodeBytes(nil), // 0
|
||||
}
|
||||
return rlpEncodeList(items)
|
||||
}
|
||||
|
||||
// rlpEncodeUint encodes a uint64 as RLP.
|
||||
func rlpEncodeUint(v uint64) []byte {
|
||||
if v == 0 {
|
||||
return []byte{0x80}
|
||||
}
|
||||
b := new(big.Int).SetUint64(v).Bytes()
|
||||
return rlpEncodeBytes(b)
|
||||
}
|
||||
|
||||
// rlpEncodeBigInt encodes a *big.Int as RLP.
|
||||
func rlpEncodeBigInt(v *big.Int) []byte {
|
||||
if v == nil || v.Sign() == 0 {
|
||||
return []byte{0x80}
|
||||
}
|
||||
return rlpEncodeBytes(v.Bytes())
|
||||
}
|
||||
|
||||
// rlpEncodeBytes encodes a byte slice as RLP string.
|
||||
func rlpEncodeBytes(b []byte) []byte {
|
||||
if len(b) == 0 {
|
||||
return []byte{0x80}
|
||||
}
|
||||
if len(b) == 1 && b[0] < 0x80 {
|
||||
return b
|
||||
}
|
||||
if len(b) < 56 {
|
||||
return append([]byte{byte(0x80 + len(b))}, b...)
|
||||
}
|
||||
lenBytes := new(big.Int).SetUint64(uint64(len(b))).Bytes()
|
||||
header := append([]byte{byte(0xb7 + len(lenBytes))}, lenBytes...)
|
||||
return append(header, b...)
|
||||
}
|
||||
|
||||
// rlpEncodeList encodes a list of already-encoded RLP items.
|
||||
func rlpEncodeList(items [][]byte) []byte {
|
||||
var payload []byte
|
||||
for _, item := range items {
|
||||
payload = append(payload, item...)
|
||||
}
|
||||
if len(payload) < 56 {
|
||||
return append([]byte{byte(0xc0 + len(payload))}, payload...)
|
||||
}
|
||||
lenBytes := new(big.Int).SetUint64(uint64(len(payload))).Bytes()
|
||||
header := append([]byte{byte(0xf7 + len(lenBytes))}, lenBytes...)
|
||||
return append(header, payload...)
|
||||
}
|
||||
|
||||
// ethCall makes a JSON-RPC call to the Lux C-Chain.
|
||||
func (r *Relay) ethCall(ctx context.Context, method string, params interface{}) (json.RawMessage, error) {
|
||||
reqBody := map[string]interface{}{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": method,
|
||||
"params": params,
|
||||
}
|
||||
body, err := json.Marshal(reqBody)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, r.rpcURL, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := r.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var rpcResp struct {
|
||||
Result json.RawMessage `json:"result"`
|
||||
Error *struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
} `json:"error"`
|
||||
}
|
||||
if err := json.Unmarshal(respBody, &rpcResp); err != nil {
|
||||
return nil, fmt.Errorf("rpc decode: %w", err)
|
||||
}
|
||||
if rpcResp.Error != nil {
|
||||
return nil, fmt.Errorf("rpc error %d: %s", rpcResp.Error.Code, rpcResp.Error.Message)
|
||||
}
|
||||
return rpcResp.Result, nil
|
||||
}
|
||||
|
||||
// funcSelector returns the first 4 bytes of keccak256(signature).
|
||||
func funcSelector(sig string) [4]byte {
|
||||
h := crypto.Keccak256([]byte(sig))
|
||||
var sel [4]byte
|
||||
copy(sel[:], h[:4])
|
||||
return sel
|
||||
}
|
||||
|
||||
// leftPad pads b on the left with zeros to size n.
|
||||
func leftPad(b []byte, n int) []byte {
|
||||
if len(b) >= n {
|
||||
return b
|
||||
}
|
||||
padded := make([]byte, n)
|
||||
copy(padded[n-len(b):], b)
|
||||
return padded
|
||||
}
|
||||
|
||||
// rightPad pads b on the right with zeros to a multiple of align.
|
||||
func rightPad(b []byte, align int) []byte {
|
||||
if len(b)%align == 0 {
|
||||
return b
|
||||
}
|
||||
padded := make([]byte, len(b)+(align-len(b)%align))
|
||||
copy(padded, b)
|
||||
return padded
|
||||
}
|
||||
@@ -0,0 +1,356 @@
|
||||
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"fmt"
|
||||
"math/big"
|
||||
|
||||
"github.com/luxfi/crypto"
|
||||
"github.com/luxfi/crypto/secp256k1"
|
||||
"github.com/luxfi/crypto/threshold"
|
||||
|
||||
// Register threshold schemes at init time.
|
||||
_ "github.com/luxfi/crypto/cggmp21"
|
||||
_ "github.com/luxfi/crypto/threshold/bls"
|
||||
)
|
||||
|
||||
// Signer produces threshold ECDSA signatures over Teleporter-compatible proof hashes.
|
||||
//
|
||||
// Uses CGGMP21 (threshold ECDSA on secp256k1) for Lux C-Chain compatibility,
|
||||
// since Teleporter.sol verifies via ECDSA.recover(). The produced signatures
|
||||
// are indistinguishable from single-party ECDSA.
|
||||
//
|
||||
// For Bitcoin/OP_NET Taproot operations, FROST (Schnorr threshold) should be
|
||||
// used instead — see threshold.SchemeFROST.
|
||||
//
|
||||
// Supported schemes via github.com/luxfi/crypto/threshold:
|
||||
// - SchemeCMP (CGGMP21): threshold ECDSA on secp256k1 — for EVM chains
|
||||
// - SchemeFROST: threshold Schnorr — for Bitcoin Taproot
|
||||
// - SchemeBLS: BLS12-381 threshold — for Lux Warp internal
|
||||
// - SchemeCorona: lattice-based — post-quantum future
|
||||
type Signer struct {
|
||||
// Single-key mode (development/testing).
|
||||
key *ecdsa.PrivateKey
|
||||
|
||||
// Threshold mode (production).
|
||||
scheme threshold.Scheme
|
||||
signer threshold.Signer
|
||||
aggregator threshold.Aggregator
|
||||
groupKey threshold.PublicKey
|
||||
}
|
||||
|
||||
// NewSigner creates a single-key signer for development.
|
||||
// In production, use NewThresholdSigner with CGGMP21 key shares.
|
||||
func NewSigner(keyBytes []byte) (*Signer, error) {
|
||||
if len(keyBytes) != 32 {
|
||||
return nil, fmt.Errorf("signer: key must be 32 bytes, got %d", len(keyBytes))
|
||||
}
|
||||
key, err := crypto.ToECDSA(keyBytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("signer: invalid key: %w", err)
|
||||
}
|
||||
return &Signer{key: key}, nil
|
||||
}
|
||||
|
||||
// NewThresholdSigner creates a threshold signer using the given scheme and key share.
|
||||
// For Teleporter compatibility (ECDSA.recover), use threshold.SchemeCMP.
|
||||
func NewThresholdSigner(schemeID threshold.SchemeID, shareBytes []byte, groupKeyBytes []byte) (*Signer, error) {
|
||||
scheme, err := threshold.GetScheme(schemeID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("signer: get scheme %s: %w", schemeID, err)
|
||||
}
|
||||
|
||||
share, err := scheme.ParseKeyShare(shareBytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("signer: parse key share: %w", err)
|
||||
}
|
||||
|
||||
groupKey, err := scheme.ParsePublicKey(groupKeyBytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("signer: parse group key: %w", err)
|
||||
}
|
||||
|
||||
signer, err := scheme.NewSigner(share)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("signer: create signer: %w", err)
|
||||
}
|
||||
|
||||
aggregator, err := scheme.NewAggregator(groupKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("signer: create aggregator: %w", err)
|
||||
}
|
||||
|
||||
return &Signer{
|
||||
scheme: scheme,
|
||||
signer: signer,
|
||||
aggregator: aggregator,
|
||||
groupKey: groupKey,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Address returns the Ethereum-style address derived from the signer's public key.
|
||||
func (s *Signer) Address() [20]byte {
|
||||
if s.key != nil {
|
||||
addr := secp256k1.PubkeyToAddress(s.key.PublicKey)
|
||||
return addr
|
||||
}
|
||||
// Threshold mode: derive from group public key.
|
||||
// CGGMP21 group key is a standard secp256k1 public key.
|
||||
hash := crypto.Keccak256(s.groupKey.Bytes())
|
||||
var addr [20]byte
|
||||
copy(addr[:], hash[12:])
|
||||
return addr
|
||||
}
|
||||
|
||||
// IsThreshold returns true if this signer uses threshold signing.
|
||||
func (s *Signer) IsThreshold() bool {
|
||||
return s.scheme != nil
|
||||
}
|
||||
|
||||
// SignRaw signs a raw hash. In single-key mode, uses the local key.
|
||||
// In threshold mode, uses the threshold signing protocol.
|
||||
// Returns a 65-byte [R || S || V] signature with V = 27 or 28.
|
||||
func (s *Signer) SignRaw(hash []byte) ([]byte, error) {
|
||||
if s.key != nil {
|
||||
return s.signSingleKey(hash)
|
||||
}
|
||||
return s.signThreshold(hash)
|
||||
}
|
||||
|
||||
// SignTransaction signs a raw EIP-155 transaction and returns the RLP-encoded signed tx.
|
||||
// CRITICAL-03: All transactions are signed locally, never via eth_sendTransaction.
|
||||
// NEW-02: Accepts an explicit tx-signing key so threshold signers can still submit txs.
|
||||
func (s *Signer) SignTransaction(unsignedRLP []byte, chainID uint64) ([]byte, error) {
|
||||
if s.key == nil {
|
||||
return nil, fmt.Errorf("signer: transaction signing requires single-key mode (use Relay.txKey for threshold)")
|
||||
}
|
||||
|
||||
txHash := crypto.Keccak256(unsignedRLP)
|
||||
sig, err := crypto.Sign(txHash, s.key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("signer: sign tx: %w", err)
|
||||
}
|
||||
|
||||
// EIP-155: v = chainId * 2 + 35 + recovery_id
|
||||
v := chainID*2 + 35 + uint64(sig[64])
|
||||
r := new(big.Int).SetBytes(sig[:32])
|
||||
rr := new(big.Int).SetBytes(sig[32:64])
|
||||
|
||||
// Re-encode as signed tx: RLP([nonce, gasPrice, gasLimit, to, value, data, v, r, s])
|
||||
// We need to decode the unsigned tx, replace the last 3 items, and re-encode.
|
||||
// The unsigned RLP has [nonce, gasPrice, gasLimit, to, value, data, chainId, 0, 0]
|
||||
// We replace chainId->v, 0->r, 0->s.
|
||||
return replaceEIP155Sig(unsignedRLP, v, r, rr)
|
||||
}
|
||||
|
||||
// replaceEIP155Sig takes unsigned EIP-155 RLP and replaces the trailing (chainId, 0, 0) with (v, r, s).
|
||||
func replaceEIP155Sig(unsignedRLP []byte, v uint64, r, ss *big.Int) ([]byte, error) {
|
||||
// Decode the RLP list to get the first 6 fields, then append v, r, s.
|
||||
items, err := rlpDecodeList(unsignedRLP)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decode unsigned tx: %w", err)
|
||||
}
|
||||
if len(items) < 9 {
|
||||
return nil, fmt.Errorf("unsigned tx has %d fields, expected 9", len(items))
|
||||
}
|
||||
|
||||
// Keep first 6 items (nonce, gasPrice, gasLimit, to, value, data), replace last 3.
|
||||
signed := make([][]byte, 9)
|
||||
copy(signed[:6], items[:6])
|
||||
signed[6] = rlpEncodeUint(v)
|
||||
signed[7] = rlpEncodeBytes(r.Bytes())
|
||||
signed[8] = rlpEncodeBytes(ss.Bytes())
|
||||
return rlpEncodeList(signed), nil
|
||||
}
|
||||
|
||||
// rlpDecodeList decodes a top-level RLP list and returns the raw RLP-encoded items.
|
||||
func rlpDecodeList(data []byte) ([][]byte, error) {
|
||||
if len(data) == 0 {
|
||||
return nil, fmt.Errorf("empty RLP")
|
||||
}
|
||||
prefix := data[0]
|
||||
var payload []byte
|
||||
if prefix >= 0xc0 && prefix <= 0xf7 {
|
||||
length := int(prefix - 0xc0)
|
||||
payload = data[1 : 1+length]
|
||||
} else if prefix >= 0xf8 {
|
||||
lenLen := int(prefix - 0xf7)
|
||||
length := new(big.Int).SetBytes(data[1 : 1+lenLen]).Uint64()
|
||||
payload = data[1+lenLen : 1+lenLen+int(length)]
|
||||
} else {
|
||||
return nil, fmt.Errorf("not an RLP list: prefix 0x%x", prefix)
|
||||
}
|
||||
|
||||
var items [][]byte
|
||||
for len(payload) > 0 {
|
||||
itemLen := rlpItemLen(payload)
|
||||
items = append(items, payload[:itemLen])
|
||||
payload = payload[itemLen:]
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
// rlpItemLen returns the total encoded length of the next RLP item.
|
||||
func rlpItemLen(data []byte) int {
|
||||
if len(data) == 0 {
|
||||
return 0
|
||||
}
|
||||
prefix := data[0]
|
||||
switch {
|
||||
case prefix < 0x80:
|
||||
return 1
|
||||
case prefix <= 0xb7:
|
||||
return 1 + int(prefix-0x80)
|
||||
case prefix <= 0xbf:
|
||||
lenLen := int(prefix - 0xb7)
|
||||
length := new(big.Int).SetBytes(data[1 : 1+lenLen]).Uint64()
|
||||
return 1 + lenLen + int(length)
|
||||
case prefix <= 0xf7:
|
||||
return 1 + int(prefix-0xc0)
|
||||
default:
|
||||
lenLen := int(prefix - 0xf7)
|
||||
length := new(big.Int).SetBytes(data[1 : 1+lenLen]).Uint64()
|
||||
return 1 + lenLen + int(length)
|
||||
}
|
||||
}
|
||||
|
||||
// SignDeposit signs a deposit proof matching Teleporter.mintDeposit:
|
||||
//
|
||||
// keccak256(abi.encode(bytes32("DEPOSIT"), srcChainId, depositNonce, recipient, amount))
|
||||
//
|
||||
// Wrapped with EIP-191 prefix before signing.
|
||||
func (s *Signer) SignDeposit(srcChainID, nonce uint64, recipient [20]byte, amount *big.Int) ([]byte, error) {
|
||||
msg := packDeposit(srcChainID, nonce, recipient, amount)
|
||||
return s.signEthMessage(msg)
|
||||
}
|
||||
|
||||
// SignBacking signs a backing attestation matching Teleporter.updateBacking:
|
||||
//
|
||||
// keccak256(abi.encode(bytes32("BACKING"), srcChainId, totalBacking, timestamp))
|
||||
func (s *Signer) SignBacking(srcChainID uint64, totalBacking *big.Int, timestamp uint64) ([]byte, error) {
|
||||
msg := packBacking(srcChainID, totalBacking, timestamp)
|
||||
return s.signEthMessage(msg)
|
||||
}
|
||||
|
||||
// signEthMessage computes keccak256(data), wraps with EIP-191, signs.
|
||||
func (s *Signer) signEthMessage(data []byte) ([]byte, error) {
|
||||
msgHash := crypto.Keccak256(data)
|
||||
|
||||
prefix := []byte("\x19Ethereum Signed Message:\n32")
|
||||
ethHash := crypto.Keccak256(append(prefix, msgHash...))
|
||||
|
||||
if s.key != nil {
|
||||
return s.signSingleKey(ethHash)
|
||||
}
|
||||
return s.signThreshold(ethHash)
|
||||
}
|
||||
|
||||
// signSingleKey signs with a single ECDSA key (development mode).
|
||||
func (s *Signer) signSingleKey(hash []byte) ([]byte, error) {
|
||||
sig, err := crypto.Sign(hash, s.key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("signer: sign: %w", err)
|
||||
}
|
||||
// crypto.Sign returns [R || S || V] with V in {0,1}
|
||||
// Teleporter expects V = 27 or 28
|
||||
sig[64] += 27
|
||||
return sig, nil
|
||||
}
|
||||
|
||||
// signThreshold signs via CGGMP21 threshold protocol.
|
||||
// Each watcher produces a share; shares are aggregated into a standard ECDSA sig.
|
||||
func (s *Signer) signThreshold(hash []byte) ([]byte, error) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Generate nonce commitment (round 1 of signing).
|
||||
commitment, nonceState, err := s.signer.NonceGen(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("signer: nonce gen: %w", err)
|
||||
}
|
||||
|
||||
// Create our signature share (round 2).
|
||||
// In production, signers list comes from ZAP consensus on which watchers participate.
|
||||
signers := []int{s.signer.Index()}
|
||||
share, err := s.signer.SignShare(ctx, hash, signers, nonceState)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("signer: sign share: %w", err)
|
||||
}
|
||||
|
||||
// In production: broadcast share + commitment via ZAP, collect t shares,
|
||||
// then aggregate. For now, return the share for external aggregation.
|
||||
//
|
||||
// When running with multiple watchers:
|
||||
// 1. Each watcher calls SignShare() and broadcasts via ZAP
|
||||
// 2. Coordinator collects t+1 shares
|
||||
// 3. Coordinator calls Aggregate() to produce final sig
|
||||
// 4. Final sig is standard 65-byte [R || S || V] ECDSA
|
||||
_ = commitment // broadcast this via ZAP to other watchers
|
||||
|
||||
// Single-watcher fallback: aggregate with just our share.
|
||||
// This only works in threshold=1 mode for testing.
|
||||
sig, err := s.aggregator.Aggregate(ctx, hash, []threshold.SignatureShare{share}, []threshold.NonceCommitment{commitment})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("signer: aggregate: %w", err)
|
||||
}
|
||||
|
||||
result := sig.Bytes()
|
||||
if len(result) == 65 && result[64] < 27 {
|
||||
result[64] += 27 // Normalize V for EIP-155
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// packDeposit encodes matching Solidity abi.encode(bytes32("DEPOSIT"), srcChainId, nonce, recipient, amount).
|
||||
// NEW-01: bytes32 tags are fixed-size — simply right-padded to 32 bytes.
|
||||
func packDeposit(srcChainID, nonce uint64, recipient [20]byte, amount *big.Int) []byte {
|
||||
var buf []byte
|
||||
buf = append(buf, abiEncodeBytes32Tag("DEPOSIT")...)
|
||||
buf = append(buf, uint256Bytes(new(big.Int).SetUint64(srcChainID))...)
|
||||
buf = append(buf, uint256Bytes(new(big.Int).SetUint64(nonce))...)
|
||||
buf = append(buf, leftPad(recipient[:], 32)...) // abi.encode pads address to 32 bytes
|
||||
buf = append(buf, uint256Bytes(amount)...)
|
||||
return buf
|
||||
}
|
||||
|
||||
// packYield encodes matching Solidity abi.encode(bytes32("YIELD"), srcChainId, yieldNonce, amount).
|
||||
func packYield(srcChainID, nonce uint64, amount *big.Int) []byte {
|
||||
var buf []byte
|
||||
buf = append(buf, abiEncodeBytes32Tag("YIELD")...)
|
||||
buf = append(buf, uint256Bytes(new(big.Int).SetUint64(srcChainID))...)
|
||||
buf = append(buf, uint256Bytes(new(big.Int).SetUint64(nonce))...)
|
||||
buf = append(buf, uint256Bytes(amount)...)
|
||||
return buf
|
||||
}
|
||||
|
||||
// packBacking encodes matching Solidity abi.encode(bytes32("BACKING"), srcChainId, totalBacking, timestamp).
|
||||
func packBacking(srcChainID uint64, totalBacking *big.Int, timestamp uint64) []byte {
|
||||
var buf []byte
|
||||
buf = append(buf, abiEncodeBytes32Tag("BACKING")...)
|
||||
buf = append(buf, uint256Bytes(new(big.Int).SetUint64(srcChainID))...)
|
||||
buf = append(buf, uint256Bytes(totalBacking)...)
|
||||
buf = append(buf, uint256Bytes(new(big.Int).SetUint64(timestamp))...)
|
||||
return buf
|
||||
}
|
||||
|
||||
// abiEncodeBytes32Tag encodes a short ASCII tag as Solidity bytes32("TAG") does:
|
||||
// right-padded with zeros to exactly 32 bytes. Matches abi.encode(bytes32(...)).
|
||||
func abiEncodeBytes32Tag(tag string) []byte {
|
||||
buf := make([]byte, 32)
|
||||
copy(buf, []byte(tag))
|
||||
return buf
|
||||
}
|
||||
|
||||
// uint256Bytes returns a 32-byte big-endian representation of n.
|
||||
func uint256Bytes(n *big.Int) []byte {
|
||||
b := make([]byte, 32)
|
||||
if n == nil {
|
||||
return b
|
||||
}
|
||||
raw := n.Bytes()
|
||||
copy(b[32-len(raw):], raw)
|
||||
return b
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"math/big"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAbiEncodeBytes32Tag(t *testing.T) {
|
||||
got := abiEncodeBytes32Tag("DEPOSIT")
|
||||
// Solidity: bytes32("DEPOSIT") = 0x4445504f534954000000000000000000000000000000000000000000000000000
|
||||
// Note: the expected hex in the task description has 65 hex chars (trailing 0).
|
||||
// Correct bytes32 is exactly 32 bytes = 64 hex chars.
|
||||
want := "4445504f534954000000000000000000000000000000000000000000000000000"
|
||||
// Normalize: the expected value has an extra trailing 0 (odd length). Use the
|
||||
// canonical 64-char representation.
|
||||
wantCanonical := "4445504f53495400000000000000000000000000000000000000000000000000"
|
||||
gotHex := hex.EncodeToString(got)
|
||||
if gotHex != wantCanonical {
|
||||
t.Fatalf("abiEncodeBytes32Tag(\"DEPOSIT\")\n got: 0x%s\n want: 0x%s", gotHex, wantCanonical)
|
||||
}
|
||||
// Also verify the task-provided vector (just trim trailing 0 if odd).
|
||||
if len(want)%2 != 0 {
|
||||
want = want[:len(want)-1]
|
||||
}
|
||||
if gotHex != want {
|
||||
t.Fatalf("mismatch with task vector: got %s, want %s", gotHex, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPackDepositKnownVector(t *testing.T) {
|
||||
// Known inputs:
|
||||
// tag: bytes32("DEPOSIT")
|
||||
// srcChainID: 1
|
||||
// nonce: 42
|
||||
// recipient: 0x000000000000000000000000DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF
|
||||
// amount: 1000000000000000000 (1e18)
|
||||
var recipient [20]byte
|
||||
hexDecode(t, recipient[:], "DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF")
|
||||
amount := new(big.Int).SetUint64(1_000_000_000_000_000_000)
|
||||
|
||||
packed := packDeposit(1, 42, recipient, amount)
|
||||
|
||||
// Verify structure: 5 * 32 = 160 bytes
|
||||
if len(packed) != 160 {
|
||||
t.Fatalf("packDeposit length: got %d, want 160", len(packed))
|
||||
}
|
||||
|
||||
// Verify first 32 bytes = bytes32("DEPOSIT")
|
||||
tagHex := hex.EncodeToString(packed[:32])
|
||||
wantTag := "4445504f53495400000000000000000000000000000000000000000000000000"
|
||||
if tagHex != wantTag {
|
||||
t.Fatalf("deposit tag mismatch:\n got: %s\n want: %s", tagHex, wantTag)
|
||||
}
|
||||
|
||||
// Verify srcChainID = 1 (uint256 big-endian)
|
||||
chainID := new(big.Int).SetBytes(packed[32:64])
|
||||
if chainID.Uint64() != 1 {
|
||||
t.Fatalf("srcChainID: got %d, want 1", chainID.Uint64())
|
||||
}
|
||||
|
||||
// Verify nonce = 42
|
||||
nonce := new(big.Int).SetBytes(packed[64:96])
|
||||
if nonce.Uint64() != 42 {
|
||||
t.Fatalf("nonce: got %d, want 42", nonce.Uint64())
|
||||
}
|
||||
|
||||
// Verify recipient (left-padded to 32 bytes)
|
||||
recipientHex := hex.EncodeToString(packed[96:128])
|
||||
wantRecipient := "000000000000000000000000deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"
|
||||
if recipientHex != wantRecipient {
|
||||
t.Fatalf("recipient mismatch:\n got: %s\n want: %s", recipientHex, wantRecipient)
|
||||
}
|
||||
|
||||
// Verify amount = 1e18
|
||||
gotAmount := new(big.Int).SetBytes(packed[128:160])
|
||||
if gotAmount.Cmp(amount) != 0 {
|
||||
t.Fatalf("amount: got %s, want %s", gotAmount, amount)
|
||||
}
|
||||
}
|
||||
|
||||
func hexDecode(t *testing.T, dst []byte, src string) {
|
||||
t.Helper()
|
||||
b, err := hex.DecodeString(src)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
copy(dst, b)
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// Tenant represents a white-label bridge deployment loaded from a manifest.
|
||||
type Tenant struct {
|
||||
ID string `yaml:"id" json:"id"`
|
||||
Name string `yaml:"name" json:"name"`
|
||||
Hostnames []string `yaml:"hostnames" json:"hostnames"`
|
||||
HomeChainID uint64 `yaml:"home_chain_id" json:"home_chain_id"`
|
||||
SupportedChains []string `yaml:"supported_chains" json:"supported_chains"`
|
||||
TeleporterAddresses map[string]string `yaml:"teleporter_addresses" json:"teleporter_addresses"`
|
||||
Branding Branding `yaml:"branding" json:"branding"`
|
||||
Features Features `yaml:"features" json:"features"`
|
||||
}
|
||||
|
||||
// Branding holds UI theme configuration for a tenant.
|
||||
type Branding struct {
|
||||
LogoURL string `yaml:"logo_url" json:"logo_url"`
|
||||
PrimaryColor string `yaml:"primary_color" json:"primary_color"`
|
||||
Theme string `yaml:"theme" json:"theme"`
|
||||
}
|
||||
|
||||
// Features holds feature flags for a tenant.
|
||||
type Features struct {
|
||||
Swap bool `yaml:"swap" json:"swap"`
|
||||
Yield bool `yaml:"yield" json:"yield"`
|
||||
Perps bool `yaml:"perps" json:"perps"`
|
||||
}
|
||||
|
||||
// ChainManifest is a minimal representation of a chain manifest for the API.
|
||||
// Full schema is in manifests/chains/schema.yaml; we only load what the API needs.
|
||||
type ChainManifest struct {
|
||||
ChainID uint64 `yaml:"chain_id" json:"chain_id"`
|
||||
Name string `yaml:"name" json:"name"`
|
||||
Class string `yaml:"class" json:"class"`
|
||||
Tier string `yaml:"tier" json:"tier"`
|
||||
Assets []AssetManifest `yaml:"assets" json:"assets"`
|
||||
Finality struct {
|
||||
Mode string `yaml:"mode" json:"mode"`
|
||||
Confirmations int `yaml:"confirmations" json:"confirmations"`
|
||||
} `yaml:"finality" json:"finality"`
|
||||
}
|
||||
|
||||
// AssetManifest is the asset portion of a chain manifest.
|
||||
type AssetManifest struct {
|
||||
Ticker string `yaml:"ticker" json:"ticker"`
|
||||
Decimals int `yaml:"decimals" json:"decimals"`
|
||||
RecipientEncoding string `yaml:"recipient_encoding" json:"recipient_encoding"`
|
||||
Wrapper struct {
|
||||
Symbol string `yaml:"symbol" json:"symbol"`
|
||||
Contract string `yaml:"contract" json:"contract"`
|
||||
Decimals int `yaml:"decimals" json:"decimals"`
|
||||
} `yaml:"wrapper" json:"wrapper"`
|
||||
}
|
||||
|
||||
// TenantRegistry loads and indexes tenant and chain manifests.
|
||||
// Thread-safe after Load completes.
|
||||
type TenantRegistry struct {
|
||||
mu sync.RWMutex
|
||||
tenants map[string]*Tenant // id -> Tenant
|
||||
byHost map[string]*Tenant // hostname -> Tenant
|
||||
chains map[string]*ChainManifest // manifest filename (no ext) -> ChainManifest
|
||||
chainDir string
|
||||
}
|
||||
|
||||
// NewTenantRegistry creates an empty registry.
|
||||
func NewTenantRegistry(chainDir string) *TenantRegistry {
|
||||
return &TenantRegistry{
|
||||
tenants: make(map[string]*Tenant),
|
||||
byHost: make(map[string]*Tenant),
|
||||
chains: make(map[string]*ChainManifest),
|
||||
chainDir: chainDir,
|
||||
}
|
||||
}
|
||||
|
||||
// Load reads all tenant manifests from tenantDir and all chain manifests from chainDir.
|
||||
func (r *TenantRegistry) Load(tenantDir string) error {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
// Load chain manifests.
|
||||
chainFiles, err := filepath.Glob(filepath.Join(r.chainDir, "*.yaml"))
|
||||
if err != nil {
|
||||
return fmt.Errorf("glob chains: %w", err)
|
||||
}
|
||||
for _, path := range chainFiles {
|
||||
var cm ChainManifest
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read chain %s: %w", path, err)
|
||||
}
|
||||
if err := yaml.Unmarshal(data, &cm); err != nil {
|
||||
return fmt.Errorf("parse chain %s: %w", path, err)
|
||||
}
|
||||
key := strings.TrimSuffix(filepath.Base(path), ".yaml")
|
||||
r.chains[key] = &cm
|
||||
}
|
||||
|
||||
// Load tenant manifests.
|
||||
tenantFiles, err := filepath.Glob(filepath.Join(tenantDir, "*.yaml"))
|
||||
if err != nil {
|
||||
return fmt.Errorf("glob tenants: %w", err)
|
||||
}
|
||||
for _, path := range tenantFiles {
|
||||
base := filepath.Base(path)
|
||||
if base == "schema.yaml" {
|
||||
continue
|
||||
}
|
||||
var t Tenant
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read tenant %s: %w", path, err)
|
||||
}
|
||||
if err := yaml.Unmarshal(data, &t); err != nil {
|
||||
return fmt.Errorf("parse tenant %s: %w", path, err)
|
||||
}
|
||||
if err := r.validateTenant(&t); err != nil {
|
||||
return fmt.Errorf("tenant %s: %w", t.ID, err)
|
||||
}
|
||||
r.tenants[t.ID] = &t
|
||||
for _, h := range t.Hostnames {
|
||||
r.byHost[strings.ToLower(h)] = &t
|
||||
}
|
||||
}
|
||||
|
||||
if len(r.tenants) == 0 {
|
||||
return fmt.Errorf("no tenant manifests found in %s", tenantDir)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateTenant checks that a tenant references only known chain manifests.
|
||||
func (r *TenantRegistry) validateTenant(t *Tenant) error {
|
||||
if t.ID == "" {
|
||||
return fmt.Errorf("id is required")
|
||||
}
|
||||
if len(t.Hostnames) == 0 {
|
||||
return fmt.Errorf("at least one hostname is required")
|
||||
}
|
||||
if len(t.SupportedChains) == 0 {
|
||||
return fmt.Errorf("at least one supported chain is required")
|
||||
}
|
||||
for _, ref := range t.SupportedChains {
|
||||
if _, ok := r.chains[ref]; !ok {
|
||||
return fmt.Errorf("references unknown chain manifest %q", ref)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// TenantForHostname resolves a hostname to a tenant.
|
||||
// The hostname is normalized to lowercase and port is stripped.
|
||||
func (r *TenantRegistry) TenantForHostname(hostname string) (*Tenant, error) {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
// Strip port if present (e.g. "bridge.lux.network:443").
|
||||
if idx := strings.IndexByte(hostname, ':'); idx != -1 {
|
||||
hostname = hostname[:idx]
|
||||
}
|
||||
hostname = strings.ToLower(hostname)
|
||||
|
||||
t, ok := r.byHost[hostname]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("no tenant for hostname %q", hostname)
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// ChainsForTenant returns the chain manifests for a tenant's supported chains.
|
||||
func (r *TenantRegistry) ChainsForTenant(tenantID string) ([]*ChainManifest, error) {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
t, ok := r.tenants[tenantID]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unknown tenant %q", tenantID)
|
||||
}
|
||||
|
||||
out := make([]*ChainManifest, 0, len(t.SupportedChains))
|
||||
for _, ref := range t.SupportedChains {
|
||||
cm, ok := r.chains[ref]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("tenant %s references missing chain %q", tenantID, ref)
|
||||
}
|
||||
out = append(out, cm)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// AssetsForTenant returns all assets across all supported chains for a tenant.
|
||||
func (r *TenantRegistry) AssetsForTenant(tenantID string) ([]TenantAsset, error) {
|
||||
chains, err := r.ChainsForTenant(tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var assets []TenantAsset
|
||||
for _, cm := range chains {
|
||||
for _, a := range cm.Assets {
|
||||
assets = append(assets, TenantAsset{
|
||||
ChainID: cm.ChainID,
|
||||
ChainName: cm.Name,
|
||||
Ticker: a.Ticker,
|
||||
Decimals: a.Decimals,
|
||||
Wrapper: a.Wrapper.Symbol,
|
||||
})
|
||||
}
|
||||
}
|
||||
return assets, nil
|
||||
}
|
||||
|
||||
// TenantAsset is a flattened asset view for the API.
|
||||
type TenantAsset struct {
|
||||
ChainID uint64 `json:"chain_id"`
|
||||
ChainName string `json:"chain_name"`
|
||||
Ticker string `json:"ticker"`
|
||||
Decimals int `json:"decimals"`
|
||||
Wrapper string `json:"wrapper"`
|
||||
}
|
||||
|
||||
// AllTenants returns all loaded tenants. Used for health/debug.
|
||||
func (r *TenantRegistry) AllTenants() []*Tenant {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
out := make([]*Tenant, 0, len(r.tenants))
|
||||
for _, t := range r.tenants {
|
||||
out = append(out, t)
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func setupTestManifests(t *testing.T) (tenantDir, chainDir string) {
|
||||
t.Helper()
|
||||
|
||||
base := t.TempDir()
|
||||
chainDir = filepath.Join(base, "chains")
|
||||
tenantDir = filepath.Join(base, "tenants")
|
||||
|
||||
if err := os.MkdirAll(chainDir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.MkdirAll(tenantDir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Write chain manifests.
|
||||
chains := map[string]string{
|
||||
"lux-c.yaml": `
|
||||
chain_id: 96369
|
||||
name: "Lux C-Chain"
|
||||
class: lux_native
|
||||
tier: yield_enabled
|
||||
assets:
|
||||
- ticker: LUX
|
||||
decimals: 18
|
||||
recipient_encoding: evm_address
|
||||
wrapper:
|
||||
symbol: LUX
|
||||
contract: "token/LUX.sol"
|
||||
decimals: 18
|
||||
finality:
|
||||
mode: instant
|
||||
confirmations: 1
|
||||
`,
|
||||
"ethereum.yaml": `
|
||||
chain_id: 1
|
||||
name: "Ethereum"
|
||||
class: evm
|
||||
tier: yield_enabled
|
||||
assets:
|
||||
- ticker: ETH
|
||||
decimals: 18
|
||||
recipient_encoding: evm_address
|
||||
wrapper:
|
||||
symbol: LETH
|
||||
contract: "liquid/tokens/LETH.sol"
|
||||
decimals: 18
|
||||
- ticker: USDC
|
||||
decimals: 6
|
||||
recipient_encoding: evm_address
|
||||
wrapper:
|
||||
symbol: LUSD
|
||||
contract: "liquid/tokens/LUSD.sol"
|
||||
decimals: 18
|
||||
finality:
|
||||
mode: slot_finalized
|
||||
confirmations: 64
|
||||
`,
|
||||
}
|
||||
for name, content := range chains {
|
||||
if err := os.WriteFile(filepath.Join(chainDir, name), []byte(content), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Write tenant manifests.
|
||||
tenants := map[string]string{
|
||||
"lux.yaml": `
|
||||
id: lux
|
||||
name: "Lux Network"
|
||||
hostnames: ["bridge.lux.network", "bridge.lux.io"]
|
||||
home_chain_id: 96369
|
||||
supported_chains: [lux-c, ethereum]
|
||||
teleporter_addresses:
|
||||
"96369": "kms://teleport/lux-c/teleporter"
|
||||
"1": "kms://teleport/lux-c/teleporter-eth"
|
||||
branding:
|
||||
logo_url: "/static/lux/logo.svg"
|
||||
primary_color: "#E84142"
|
||||
theme: dark
|
||||
features:
|
||||
swap: true
|
||||
yield: true
|
||||
perps: true
|
||||
`,
|
||||
"zoo.yaml": `
|
||||
id: zoo
|
||||
name: "Zoo Network"
|
||||
hostnames: ["bridge.zoo.ngo"]
|
||||
home_chain_id: 200200
|
||||
supported_chains: [lux-c]
|
||||
teleporter_addresses:
|
||||
"96369": "kms://teleport/lux-c/teleporter"
|
||||
branding:
|
||||
logo_url: "/static/zoo/logo.svg"
|
||||
primary_color: "#10B981"
|
||||
theme: dark
|
||||
features:
|
||||
swap: true
|
||||
yield: true
|
||||
perps: false
|
||||
`,
|
||||
"schema.yaml": `
|
||||
schema_version: "1.0.0"
|
||||
`,
|
||||
}
|
||||
for name, content := range tenants {
|
||||
if err := os.WriteFile(filepath.Join(tenantDir, name), []byte(content), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
return tenantDir, chainDir
|
||||
}
|
||||
|
||||
func TestTenantRegistryLoad(t *testing.T) {
|
||||
tenantDir, chainDir := setupTestManifests(t)
|
||||
r := NewTenantRegistry(chainDir)
|
||||
if err := r.Load(tenantDir); err != nil {
|
||||
t.Fatalf("Load: %v", err)
|
||||
}
|
||||
|
||||
all := r.AllTenants()
|
||||
if len(all) != 2 {
|
||||
t.Fatalf("expected 2 tenants, got %d", len(all))
|
||||
}
|
||||
}
|
||||
|
||||
func TestTenantForHostname(t *testing.T) {
|
||||
tenantDir, chainDir := setupTestManifests(t)
|
||||
r := NewTenantRegistry(chainDir)
|
||||
if err := r.Load(tenantDir); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
host string
|
||||
wantID string
|
||||
wantErr bool
|
||||
}{
|
||||
{"bridge.lux.network", "lux", false},
|
||||
{"bridge.lux.io", "lux", false},
|
||||
{"Bridge.Lux.Network", "lux", false}, // case insensitive
|
||||
{"bridge.lux.network:443", "lux", false}, // port stripped
|
||||
{"bridge.zoo.ngo", "zoo", false},
|
||||
{"bridge.unknown.com", "", true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tenant, err := r.TenantForHostname(tt.host)
|
||||
if tt.wantErr {
|
||||
if err == nil {
|
||||
t.Errorf("TenantForHostname(%q) expected error", tt.host)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("TenantForHostname(%q) error: %v", tt.host, err)
|
||||
continue
|
||||
}
|
||||
if tenant.ID != tt.wantID {
|
||||
t.Errorf("TenantForHostname(%q) = %q, want %q", tt.host, tenant.ID, tt.wantID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestChainsForTenant(t *testing.T) {
|
||||
tenantDir, chainDir := setupTestManifests(t)
|
||||
r := NewTenantRegistry(chainDir)
|
||||
if err := r.Load(tenantDir); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
chains, err := r.ChainsForTenant("lux")
|
||||
if err != nil {
|
||||
t.Fatalf("ChainsForTenant(lux): %v", err)
|
||||
}
|
||||
if len(chains) != 2 {
|
||||
t.Fatalf("expected 2 chains for lux, got %d", len(chains))
|
||||
}
|
||||
|
||||
chains, err = r.ChainsForTenant("zoo")
|
||||
if err != nil {
|
||||
t.Fatalf("ChainsForTenant(zoo): %v", err)
|
||||
}
|
||||
if len(chains) != 1 {
|
||||
t.Fatalf("expected 1 chain for zoo, got %d", len(chains))
|
||||
}
|
||||
if chains[0].ChainID != 96369 {
|
||||
t.Errorf("expected chain_id 96369, got %d", chains[0].ChainID)
|
||||
}
|
||||
|
||||
_, err = r.ChainsForTenant("nonexistent")
|
||||
if err == nil {
|
||||
t.Error("ChainsForTenant(nonexistent) expected error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAssetsForTenant(t *testing.T) {
|
||||
tenantDir, chainDir := setupTestManifests(t)
|
||||
r := NewTenantRegistry(chainDir)
|
||||
if err := r.Load(tenantDir); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assets, err := r.AssetsForTenant("lux")
|
||||
if err != nil {
|
||||
t.Fatalf("AssetsForTenant(lux): %v", err)
|
||||
}
|
||||
// lux-c has 1 asset (LUX), ethereum has 2 (ETH, USDC)
|
||||
if len(assets) != 3 {
|
||||
t.Fatalf("expected 3 assets for lux, got %d", len(assets))
|
||||
}
|
||||
|
||||
assets, err = r.AssetsForTenant("zoo")
|
||||
if err != nil {
|
||||
t.Fatalf("AssetsForTenant(zoo): %v", err)
|
||||
}
|
||||
if len(assets) != 1 {
|
||||
t.Fatalf("expected 1 asset for zoo, got %d", len(assets))
|
||||
}
|
||||
if assets[0].Ticker != "LUX" {
|
||||
t.Errorf("expected ticker LUX, got %s", assets[0].Ticker)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateTenantBadChainRef(t *testing.T) {
|
||||
base := t.TempDir()
|
||||
chainDir := filepath.Join(base, "chains")
|
||||
tenantDir := filepath.Join(base, "tenants")
|
||||
os.MkdirAll(chainDir, 0o755)
|
||||
os.MkdirAll(tenantDir, 0o755)
|
||||
|
||||
// Only lux-c chain exists.
|
||||
os.WriteFile(filepath.Join(chainDir, "lux-c.yaml"), []byte(`
|
||||
chain_id: 96369
|
||||
name: "Lux C-Chain"
|
||||
class: lux_native
|
||||
tier: yield_enabled
|
||||
assets: [{ticker: LUX, decimals: 18, recipient_encoding: evm_address, wrapper: {symbol: LUX, contract: "t.sol", decimals: 18}}]
|
||||
finality: {mode: instant, confirmations: 1}
|
||||
`), 0o644)
|
||||
|
||||
// Tenant references nonexistent chain.
|
||||
os.WriteFile(filepath.Join(tenantDir, "bad.yaml"), []byte(`
|
||||
id: bad
|
||||
name: "Bad Tenant"
|
||||
hostnames: ["bad.example.com"]
|
||||
home_chain_id: 1
|
||||
supported_chains: [lux-c, nonexistent]
|
||||
teleporter_addresses: {"96369": "0xdead"}
|
||||
branding: {logo_url: "/x", primary_color: "#000000", theme: dark}
|
||||
features: {swap: false, yield: false, perps: false}
|
||||
`), 0o644)
|
||||
|
||||
r := NewTenantRegistry(chainDir)
|
||||
err := r.Load(tenantDir)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for bad chain reference")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"math/big"
|
||||
"time"
|
||||
|
||||
"github.com/luxfi/bridge/opnet-watcher/plugins"
|
||||
)
|
||||
|
||||
// Watcher orchestrates the plugin -> signer -> relay pipeline.
|
||||
// It runs the block polling loop and the backing attestor as separate goroutines,
|
||||
// coordinating graceful shutdown via context cancellation.
|
||||
type Watcher struct {
|
||||
plugin plugins.ChainPlugin
|
||||
signer *Signer
|
||||
relay *Relay
|
||||
cfg Config
|
||||
lastPos uint64 // last processed block/slot/lt
|
||||
lastBacking *big.Int // HIGH-03: last attested backing for sanity check
|
||||
}
|
||||
|
||||
// NewWatcher assembles the full pipeline from config.
|
||||
func NewWatcher(cfg Config, plugin plugins.ChainPlugin, signer *Signer, relay *Relay) *Watcher {
|
||||
return &Watcher{
|
||||
plugin: plugin,
|
||||
signer: signer,
|
||||
relay: relay,
|
||||
cfg: cfg,
|
||||
lastPos: cfg.StartBlock,
|
||||
}
|
||||
}
|
||||
|
||||
// Run starts the watcher. It blocks until ctx is cancelled.
|
||||
func (w *Watcher) Run(ctx context.Context) error {
|
||||
log.Printf("watcher: starting chain=%s chain_id=%d source=%s lux=%s",
|
||||
w.plugin.Name(), w.plugin.ChainID(), w.cfg.SourceRPCURL, w.cfg.LuxRPCURL)
|
||||
log.Printf("watcher: teleporter=%s bridge=%s poll=%s backing=%s depth=%d",
|
||||
w.cfg.TeleporterAddr, w.cfg.SourceBridgeAddr, w.cfg.BlockPollInterval, w.cfg.BackingInterval, w.cfg.ConfirmationDepth)
|
||||
|
||||
// Start backing attestor in background.
|
||||
go w.runBackingLoop(ctx)
|
||||
|
||||
// Main event loop: poll blocks, process events.
|
||||
ticker := time.NewTicker(w.cfg.BlockPollInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Println("watcher: shutting down")
|
||||
return ctx.Err()
|
||||
case <-ticker.C:
|
||||
w.pollAndRelay(ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// pollAndRelay fetches new deposit events from the plugin and relays them.
|
||||
// CRITICAL-02: Only processes blocks with sufficient confirmations to prevent reorg-based drain.
|
||||
func (w *Watcher) pollAndRelay(ctx context.Context) {
|
||||
events, newPos, err := w.plugin.PollDeposits(ctx, w.lastPos)
|
||||
if err != nil {
|
||||
log.Printf("watcher: poll error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// CRITICAL-02: Only process blocks with confirmationDepth confirmations.
|
||||
if newPos <= w.cfg.ConfirmationDepth {
|
||||
return
|
||||
}
|
||||
confirmedPos := newPos - w.cfg.ConfirmationDepth
|
||||
|
||||
var confirmed []plugins.DepositEvent
|
||||
for _, ev := range events {
|
||||
if ev.BlockHeight <= confirmedPos {
|
||||
confirmed = append(confirmed, ev)
|
||||
}
|
||||
}
|
||||
|
||||
for _, ev := range confirmed {
|
||||
if err := w.processDeposit(ctx, ev); err != nil {
|
||||
log.Printf("watcher: deposit error block=%d nonce=%d: %v", ev.BlockHeight, ev.Nonce, err)
|
||||
}
|
||||
}
|
||||
|
||||
if confirmedPos > w.lastPos {
|
||||
if len(confirmed) > 0 {
|
||||
log.Printf("watcher: processed %d deposits through pos=%d (tip=%d depth=%d)",
|
||||
len(confirmed), confirmedPos, newPos, w.cfg.ConfirmationDepth)
|
||||
}
|
||||
w.lastPos = confirmedPos
|
||||
}
|
||||
}
|
||||
|
||||
// processDeposit signs a deposit proof and submits it to Teleporter.mintDeposit.
|
||||
func (w *Watcher) processDeposit(ctx context.Context, ev plugins.DepositEvent) error {
|
||||
log.Printf("watcher: deposit block=%d nonce=%d recipient=%x amount=%s",
|
||||
ev.BlockHeight, ev.Nonce, ev.Recipient, ev.Amount.String())
|
||||
|
||||
chainID := w.plugin.ChainID()
|
||||
|
||||
sig, err := w.signer.SignDeposit(chainID, ev.Nonce, ev.Recipient, ev.Amount)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
txHash, err := w.relay.SubmitDeposit(ctx, chainID, ev.Nonce, ev.Recipient, ev.Amount, sig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf("watcher: deposit relayed nonce=%d tx=%s", ev.Nonce, txHash)
|
||||
return nil
|
||||
}
|
||||
|
||||
// runBackingLoop periodically queries total locked from the source chain
|
||||
// and submits backing attestations to Teleporter.updateBacking.
|
||||
func (w *Watcher) runBackingLoop(ctx context.Context) {
|
||||
w.attestBacking(ctx)
|
||||
|
||||
ticker := time.NewTicker(w.cfg.BackingInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Println("backing: shutting down")
|
||||
return
|
||||
case <-ticker.C:
|
||||
w.attestBacking(ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (w *Watcher) attestBacking(ctx context.Context) {
|
||||
totalLocked, err := w.plugin.QueryBacking(ctx)
|
||||
if err != nil {
|
||||
log.Printf("backing: query failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// HIGH-03: Sanity check — backing cannot change by more than MaxBackingChangePct per attestation.
|
||||
if w.lastBacking != nil && w.lastBacking.Sign() > 0 {
|
||||
maxChange := new(big.Int).Mul(w.lastBacking, new(big.Int).SetUint64(w.cfg.MaxBackingChangePct))
|
||||
maxChange.Div(maxChange, big.NewInt(100))
|
||||
diff := new(big.Int).Sub(totalLocked, w.lastBacking)
|
||||
if diff.Sign() < 0 {
|
||||
diff.Neg(diff)
|
||||
}
|
||||
if diff.Cmp(maxChange) > 0 {
|
||||
log.Printf("backing: REJECTED — change too large: %s -> %s (max %d%% = %s)",
|
||||
w.lastBacking.String(), totalLocked.String(), w.cfg.MaxBackingChangePct, maxChange.String())
|
||||
return
|
||||
}
|
||||
// Log significant changes as warnings even within bounds.
|
||||
tenPct := new(big.Int).Div(w.lastBacking, big.NewInt(10))
|
||||
if diff.Cmp(tenPct) > 0 {
|
||||
log.Printf("backing: WARNING — significant change: %s -> %s", w.lastBacking.String(), totalLocked.String())
|
||||
}
|
||||
}
|
||||
|
||||
chainID := w.plugin.ChainID()
|
||||
timestamp := uint64(time.Now().Unix())
|
||||
|
||||
sig, err := w.signer.SignBacking(chainID, totalLocked, timestamp)
|
||||
if err != nil {
|
||||
log.Printf("backing: sign failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// CRITICAL-01: Pass timestamp to SubmitBacking (signed by MPC, validated on-chain).
|
||||
txHash, err := w.relay.SubmitBacking(ctx, chainID, totalLocked, timestamp, sig)
|
||||
if err != nil {
|
||||
log.Printf("backing: submit failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
w.lastBacking = totalLocked
|
||||
log.Printf("backing: attested chain=%s total=%s tx=%s", w.plugin.Name(), totalLocked.String(), txHash)
|
||||
}
|
||||
Reference in New Issue
Block a user