feat: add gateway for DEX routing

- Add gateway package for multi-DEX routing
- Add Lux and Uniswap providers
- Add server and registry components
- Update dependencies
This commit is contained in:
Zach Kelling
2025-12-28 11:00:04 -08:00
parent b00e6a8403
commit 14f3436b91
21 changed files with 5516 additions and 798 deletions
+171 -15
View File
@@ -1,19 +1,175 @@
Copyright (c) 2025 Lux Industries Inc. All rights reserved.
Lux Research License with Patent Reservation
Version 1.0, December 2025
This software and associated documentation files (the "Software") are the
proprietary and confidential property of Lux Industries Inc.
Copyright (c) 2020-2025 Lux Industries Inc.
All rights reserved.
No part of this Software may be reproduced, distributed, or transmitted in any
form or by any means, including photocopying, recording, or other electronic or
mechanical methods, without the prior written permission of Lux Industries Inc.
PATENT PENDING TECHNOLOGY
Contact: oss@lux.network
For permission requests, write to:
Lux Industries Inc.
Email: legal@lux.network
================================================================================
IMPORTANT NOTICE
================================================================================
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
LUX INDUSTRIES INC. BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
This software contains patent-pending technology. Key innovations include but
are not limited to:
DEX INNOVATIONS:
- Quantum-Resistant DAG Consensus with Hybrid BLS+Corona Signatures
- Multi-Backend Hardware-Accelerated Order Matching (CPU/GPU/FPGA)
- FPGA-Accelerated Order Processing Pipeline (48-byte wire protocol)
- Multi-Source Price Oracle with Q-Chain Quantum Finality Verification
- Three-Mode Margin Trading with Insurance Fund & ADL
- Cross-Chain Bridge with Fraud Proofs and Liquidity Rebalancing
- Lock-Free Order Pool Architecture with Zero-Allocation Design
Commercial use of any patented technology requires a separate license.
Contact oss@lux.network for commercial licensing inquiries.
================================================================================
LICENSE TERMS
================================================================================
1. DEFINITIONS
"Software" means the source code, object code, documentation, and any
related materials included in this repository.
"Research Use" means use of the Software solely for non-commercial
academic research, education, personal study, or evaluation purposes.
"Commercial Use" means any use of the Software or any patent claims
covered by the Software in connection with a product or service offered
for sale or fee, or any internal use by a for-profit entity, or any use
to generate revenue directly or indirectly.
"Patent Claims" means any patent claims owned or controlled by Lux
Industries Inc. that cover any aspect of the Software's implementation
or any method, system, or process described therein.
"Lux Network" means the decentralized blockchain network operated using
software distributed by Lux Industries Inc.
2. GRANT OF RESEARCH LICENSE
Subject to the terms and conditions of this License, Lux Industries Inc.
hereby grants you a limited, non-exclusive, non-transferable, revocable,
royalty-free license to:
(a) Use, copy, and modify the Software solely for Research Use;
(b) Create derivative works of the Software solely for Research Use;
(c) Publish academic papers and research findings based on the Software,
provided that proper attribution is given; and
(d) Contribute modifications back to the original repository.
3. RESTRICTIONS
You may NOT:
(a) Use the Software or any Patent Claims for any Commercial Use without
first obtaining a commercial license from Lux Industries Inc.;
(b) Use the Software to create or operate any blockchain, DEX, exchange,
trading platform, or financial system that competes with or is
similar to the Lux Network;
(c) Sublicense, sell, lease, or otherwise transfer rights in the
Software to any third party;
(d) Remove or alter any copyright, patent, trademark, or attribution
notices contained in the Software;
(e) Use the Software in any manner that infringes the Patent Claims
without authorization; or
(f) File any patent applications covering technology substantially
derived from the Software.
4. PATENT RIGHTS RESERVATION
ALL PATENT RIGHTS ARE EXPRESSLY RESERVED by Lux Industries Inc.
This License does NOT grant any license or rights under any Patent Claims.
The grant of a copyright license herein does NOT include any express or
implied patent license.
Any use of the Software that would infringe Patent Claims requires a
separate patent license, which must be obtained in writing from:
Lux Industries Inc.
Email: oss@lux.network
Subject: Patent License Request
Unauthorized commercial use or infringement of Patent Claims may result
in legal action for patent infringement and copyright infringement.
5. LUX NETWORK PRIMARY NETWORK EXCEPTION
Notwithstanding the above restrictions, this License grants an automatic
license to use the Software and Patent Claims solely for the purpose of
operating a node on the Lux Network primary network (mainnet and testnet),
subject to the network's terms of service and validator requirements.
This exception does NOT extend to:
- Creating competing networks, subnets, or chains
- Creating competing DEX, exchange, or trading platforms
- Commercial custody, bridge, or MPC services
- Any other commercial exploitation
6. CONTRIBUTIONS
By submitting contributions to this repository, you agree to license
your contributions under the same terms as this License and assign all
patent rights in your contributions to Lux Industries Inc.
7. DISCLAIMER OF WARRANTY
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT.
8. LIMITATION OF LIABILITY
IN NO EVENT SHALL LUX INDUSTRIES INC. BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY KIND ARISING OUT
OF THE USE OF OR INABILITY TO USE THE SOFTWARE.
9. TERMINATION
This License and all rights granted hereunder will terminate immediately
upon any breach of this License by you.
10. GOVERNING LAW
This License shall be governed by the laws of the State of Delaware,
United States.
11. COMMERCIAL LICENSING
For commercial licensing inquiries, please contact:
Lux Industries Inc.
Email: oss@lux.network
Commercial licenses are available for:
- Operating exchanges or trading platforms
- Custody and MPC services
- Bridge and cross-chain services
- Enterprise blockchain solutions
================================================================================
ATTRIBUTION
================================================================================
When using this Software for research purposes, please cite:
Lux Network DEX: Quantum-Resistant Decentralized Exchange
Copyright (c) 2020-2025 Lux Industries Inc.
https://lux.network
================================================================================
END OF LICENSE TERMS
+149
View File
@@ -1777,3 +1777,152 @@ A comprehensive code review was conducted focusing on compound word usage, code
**Estimated Effort:** 2-3 sprints for high priority fixes, 1-2 quarters for complete refactoring.
See CODE_REVIEW_2025-12-12.md for detailed recommendations with file:line references.
---
## API Gateway (2025-12-26)
### Overview
A new provider-based API gateway has been added to `pkg/gateway/` that aggregates multiple DEX providers (Uniswap, native Lux) with priority-based routing and fallback support.
### Architecture
```
┌─────────────────────────────────────────────────────────────┐
│ API GATEWAY │
├─────────────────────────────────────────────────────────────┤
│ │
│ HTTP Server (pkg/gateway/server.go) │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ /v1/quote /v1/pools /v1/price /v1/leads │ │
│ │ /v1/quotes /v1/positions /v1/prices /v1/events │ │
│ └───────────────────────────────────────────────────────┘ │
│ ↓ │
│ Router (pkg/gateway/router.go) │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ - Multi-provider aggregation │ │
│ │ - Best quote selection (by output amount) │ │
│ │ - Fallback on provider failure │ │
│ │ - Parallel quote fetching │ │
│ └───────────────────────────────────────────────────────┘ │
│ ↓ │
│ Provider Registry (pkg/gateway/registry.go) │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ Priority 10: Lux Native (Lux/Zoo chains) │ │
│ │ Priority 100: Uniswap (All chains, fallback) │ │
│ └───────────────────────────────────────────────────────┘ │
│ ↓ │
│ Providers │
│ ┌─────────────────────┐ ┌─────────────────────────────┐ │
│ │ lux/provider.go │ │ uniswap/provider.go │ │
│ │ - Native DEX pools │ │ - api.uniswap.org │ │
│ │ - Precompile 0x0400 │ │ - liquidity.backend-prod │ │
│ │ - Oracle integration│ │ - entry-gateway.backend │ │
│ └─────────────────────┘ └─────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
### Provider Interfaces
```go
// Core interfaces in pkg/gateway/provider.go
type QuoteProvider interface {
GetQuote(ctx, req QuoteRequest) (*SwapQuote, error)
GetQuotes(ctx, req QuoteRequest) ([]SwapQuote, error)
}
type SwapProvider interface {
QuoteProvider
BuildSwap(ctx, quote, recipient, deadline) (*SwapTransaction, error)
}
type LiquidityProvider interface {
GetPools(ctx, req PoolsRequest) ([]Pool, error)
GetPositions(ctx, req PositionsRequest) ([]Position, error)
}
type ConversionProvider interface {
CreateLead(ctx, lead) (*ConversionLead, error)
TrackEvent(ctx, event) error
}
```
### Uniswap Provider (Fallback)
Delegates to 3 Uniswap APIs:
| API | Endpoint | Purpose |
|-----|----------|---------|
| Core API | `api.uniswap.org` | Quotes, tokens, prices |
| Liquidity | `liquidity.backend-prod.api.uniswap.org` | Pools, positions |
| Conversion | `entry-gateway.backend-*.api.uniswap.org` | Analytics tracking |
### Lux Native Provider (Priority)
Stubs ready for implementation:
- Quote from DEX precompile (0x0400)
- Pool queries from PoolManager
- Native token list for Lux/Zoo chains
- Oracle integration from `pkg/lx/oracle.go`
### Supported Chains
| Chain | ID | Native Provider | Uniswap Fallback |
|-------|-----|-----------------|------------------|
| Lux | 96369 | Yes (priority) | Yes |
| Zoo | 200200 | Yes (priority) | Yes |
| Ethereum | 1 | No | Yes |
| Arbitrum | 42161 | No | Yes |
| Optimism | 10 | No | Yes |
| Polygon | 137 | No | Yes |
| Base | 8453 | No | Yes |
| BNB | 56 | No | Yes |
### Usage
```bash
# Start gateway server
go run cmd/gateway/main.go -addr :8080
# Get best quote from all providers
curl -X POST http://localhost:8080/v1/quote \
-H "Content-Type: application/json" \
-d '{
"tokenIn": "0x...",
"tokenOut": "0x...",
"chainId": 96369,
"amount": "1000000000000000000",
"isExactIn": true
}'
# Health check
curl http://localhost:8080/health
# List providers
curl http://localhost:8080/providers
```
### Tests
```bash
# Run gateway tests (12 tests, all passing)
go test -v ./pkg/gateway/...
```
### Future Work
1. **Lux Native Implementation**
- Connect to DEX precompile at 0x0400
- Integrate with orderbook in `pkg/lx/`
- Add native oracle prices from Chainlink/Pyth sources
2. **Caching Layer**
- Quote caching with TTL
- Pool state caching
- Token list caching
3. **Advanced Routing**
- Multi-hop route optimization
- Split routes across providers
- MEV-protected execution
+105
View File
@@ -0,0 +1,105 @@
// Gateway API server that aggregates multiple DEX providers.
// Connects to real Lux nodes for on-chain pool data with Uniswap fallback.
package main
import (
"flag"
"log"
"os"
"time"
"github.com/luxfi/dex/pkg/gateway"
"github.com/luxfi/dex/pkg/gateway/lux"
"github.com/luxfi/dex/pkg/gateway/uniswap"
)
func main() {
// Command line flags
addr := flag.String("addr", ":8080", "Server address")
uniswapAPIKey := flag.String("uniswap-api-key", "", "Uniswap API key (optional)")
enableFallback := flag.Bool("fallback", true, "Enable fallback to next provider on failure")
// RPC endpoints for Lux chains
luxRPC := flag.String("lux-rpc", "", "Lux mainnet C-Chain RPC endpoint")
zooRPC := flag.String("zoo-rpc", "", "Zoo chain RPC endpoint")
flag.Parse()
// Check environment variables for RPC endpoints
if *luxRPC == "" {
*luxRPC = os.Getenv("LUX_RPC_URL")
}
if *zooRPC == "" {
*zooRPC = os.Getenv("ZOO_RPC_URL")
}
// Default endpoints if not provided
if *luxRPC == "" {
*luxRPC = "http://127.0.0.1:9630/ext/bc/C/rpc"
}
if *zooRPC == "" {
*zooRPC = "http://127.0.0.1:9630/ext/bc/2iJykKjE7gpWNjGUvGG6fVtj7u5Tbvo89CVCu6gjNPCnEdCVpY/rpc"
}
// Create gateway with configuration
config := gateway.GatewayConfig{
DefaultChainID: gateway.ChainIDLux,
EnableFallback: *enableFallback,
CacheEnabled: true,
CacheTTLSeconds: 60,
}
gw := gateway.New(config)
// Register Lux native provider with RPC connectivity (highest priority)
rpcEndpoints := map[gateway.ChainID]string{
gateway.ChainIDLux: *luxRPC,
gateway.ChainIDZoo: *zooRPC,
}
luxProvider := lux.NewProvider(lux.ProviderConfig{
Name: "lux",
Priority: 10, // Lower number = higher priority
Chains: []gateway.ChainID{gateway.ChainIDLux, gateway.ChainIDZoo},
RPCEndpoints: rpcEndpoints,
})
if err := gw.RegisterProvider(luxProvider); err != nil {
log.Fatalf("Failed to register Lux provider: %v", err)
}
log.Printf("Registered Lux native provider (priority: 10)")
log.Printf(" LUX RPC: %s", *luxRPC)
log.Printf(" ZOO RPC: %s", *zooRPC)
// Register Uniswap provider (fallback for all chains)
uniswapProvider := uniswap.NewProvider(uniswap.ProviderConfig{
Name: "uniswap",
Priority: 100, // Lower priority - used as fallback
APIKey: *uniswapAPIKey,
Timeout: 30 * time.Second,
Chains: []gateway.ChainID{
gateway.ChainIDEthereum,
gateway.ChainIDArbitrum,
gateway.ChainIDOptimism,
gateway.ChainIDPolygon,
gateway.ChainIDBase,
gateway.ChainIDBNB,
// Also supports Lux chains as fallback
gateway.ChainIDLux,
gateway.ChainIDZoo,
},
})
if err := gw.RegisterProvider(uniswapProvider); err != nil {
log.Fatalf("Failed to register Uniswap provider: %v", err)
}
log.Println("Registered Uniswap provider (priority: 100, fallback)")
// Start gateway with graceful shutdown
log.Printf("Gateway server starting on %s", *addr)
log.Println("Provider priority: lux (10) > uniswap (100)")
log.Println("Supported chains: Lux (96369), Zoo (200200), Ethereum (1), Arbitrum (42161), Optimism (10), Polygon (137), Base (8453), BNB (56)")
if err := gw.StartWithGracefulShutdown(*addr); err != nil {
log.Fatalf("Server error: %v", err)
}
}
+30
View File
@@ -0,0 +1,30 @@
// source.config.ts
import {
defineConfig,
defineDocs
} from "@hanzo/mdx/config";
import rehypePrettyCode from "rehype-pretty-code";
var source_config_default = defineConfig({
mdxOptions: {
rehypePlugins: [
[
rehypePrettyCode,
{
theme: {
dark: "github-dark-dimmed",
light: "github-light"
},
keepBackground: false,
defaultLang: "go"
}
]
]
}
});
var docs = defineDocs({
dir: "content/docs"
});
export {
source_config_default as default,
docs
};
+1236 -736
View File
File diff suppressed because it is too large Load Diff
Executable
BIN
View File
Binary file not shown.
Executable
BIN
View File
Binary file not shown.
+30 -19
View File
@@ -1,41 +1,52 @@
module github.com/luxfi/dex
go 1.24.5
toolchain go1.24.6
go 1.25.5
require (
github.com/gorilla/websocket v1.5.3
github.com/luxfi/database v1.1.13
github.com/luxfi/log v1.1.22
github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674
github.com/luxfi/database v1.2.15
github.com/luxfi/log v1.1.26
github.com/luxfi/mlx v0.2.0
github.com/nats-io/nats.go v1.44.0
github.com/pebbe/zmq4 v1.4.0
github.com/shopspring/decimal v1.4.0
github.com/stretchr/testify v1.10.0
google.golang.org/grpc v1.74.2
google.golang.org/protobuf v1.36.7
github.com/stretchr/testify v1.11.1
google.golang.org/grpc v1.75.1
google.golang.org/protobuf v1.36.11
)
require (
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 // indirect
github.com/bits-and-blooms/bitset v1.24.4 // indirect
github.com/consensys/gnark-crypto v0.19.2 // indirect
github.com/crate-crypto/go-eth-kzg v1.4.0 // indirect
github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
github.com/ethereum/c-kzg-4844/v2 v2.1.5 // indirect
github.com/ethereum/go-verkle v0.2.2 // indirect
github.com/holiman/uint256 v1.3.2 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/luxfi/crypto v1.3.2 // indirect
github.com/luxfi/ids v1.0.2 // indirect
github.com/luxfi/cache v1.0.0 // indirect
github.com/luxfi/crypto v1.17.26 // indirect
github.com/luxfi/geth v1.16.64 // indirect
github.com/luxfi/ids v1.2.5 // indirect
github.com/mr-tron/base58 v1.2.0 // indirect
github.com/nats-io/nkeys v0.4.11 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
go.opentelemetry.io/otel/sdk v1.37.0 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/supranational/blst v0.3.16 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
golang.org/x/crypto v0.41.0 // indirect
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 // indirect
golang.org/x/net v0.43.0 // indirect
golang.org/x/sys v0.35.0 // indirect
golang.org/x/text v0.28.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250721164621-a45f3dfb1074 // indirect
go.uber.org/zap v1.27.1 // indirect
golang.org/x/crypto v0.46.0 // indirect
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.32.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+56 -28
View File
@@ -1,7 +1,11 @@
github.com/DataDog/zstd v1.5.7 h1:ybO8RBeh29qrxIhCA9E8gKY6xfONU9T6G6aP9DTKfLE=
github.com/DataDog/zstd v1.5.7/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw=
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 h1:1zYrtlhrZ6/b6SAjLSfKzWtdgqK0U+HtH/VcBWh1BaU=
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bits-and-blooms/bitset v1.24.4 h1:95H15Og1clikBrKr/DuzMXkQzECs1M6hhoGXLwLQOZE=
github.com/bits-and-blooms/bitset v1.24.4/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cockroachdb/errors v1.11.3 h1:5bA+k2Y6r+oz/6Z/RFlNeVCesGARKuC6YymtcDrbC/I=
@@ -16,6 +20,12 @@ github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwP
github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg=
github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo=
github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ=
github.com/consensys/gnark-crypto v0.19.2 h1:qrEAIXq3T4egxqiliFFoNrepkIWVEeIYwt3UL0fvS80=
github.com/consensys/gnark-crypto v0.19.2/go.mod h1:rT23F0XSZqE0mUA0+pRtnL56IbPxs6gp4CeRsBk4XS0=
github.com/crate-crypto/go-eth-kzg v1.4.0 h1:WzDGjHk4gFg6YzV0rJOAsTK4z3Qkz5jd4RE3DAvPFkg=
github.com/crate-crypto/go-eth-kzg v1.4.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI=
github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a h1:W8mUrRp6NOVl3J+MYp5kPMoUZPp7aOYHtaua31lwRHg=
github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a/go.mod h1:sTwzHBvIzm2RfVCGNEBZgRyjwK40bVoun3ZnGOCafNM=
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/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
@@ -26,6 +36,10 @@ github.com/dgraph-io/ristretto/v2 v2.2.0 h1:bkY3XzJcXoMuELV8F+vS8kzNgicwQFAaGINA
github.com/dgraph-io/ristretto/v2 v2.2.0/go.mod h1:RZrm63UmcBAaYWC1DotLYBmTvgkrs0+XhBd7Npn7/zI=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/ethereum/c-kzg-4844/v2 v2.1.5 h1:aVtoLK5xwJ6c5RiqO8g8ptJ5KU+2Hdquf6G3aXiHh5s=
github.com/ethereum/c-kzg-4844/v2 v2.1.5/go.mod h1:u59hRTTah4Co6i9fDWtiCjTrblJv0UwsqZKCc0GfgUs=
github.com/ethereum/go-verkle v0.2.2 h1:I2W0WjnrFUIzzVPwm8ykY+7pL2d4VhlsePn4j7cnFk8=
github.com/ethereum/go-verkle v0.2.2/go.mod h1:M3b90YRnzqKyyzBEWJGqj8Qff4IDeXnzFw0P9bFw3uk=
github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps=
github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
@@ -46,8 +60,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/rpc v1.2.1 h1:yC+LMV5esttgpVvNORL/xX4jvTTEUE30UZhZ5JF7K9k=
github.com/gorilla/rpc v1.2.1/go.mod h1:uNpOihAlF5xRFLuTYhfR0yfCTm0WTQSQttkMSptRfGk=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA=
github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
@@ -56,16 +70,26 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/luxfi/cache v1.0.0 h1:OcqGi/qs9ky+7O8fR6WMZtFsunvA40LBTg3QRQqJurs=
github.com/luxfi/cache v1.0.0/go.mod h1:BEvpffvV3SXs/KSa9nUC1TszxKwUdIHwIaoXpeiYU+Y=
github.com/luxfi/crypto v1.3.2 h1:3qKhhjzFYdT6FzbEuOp+6/vK2qdZ4EVAEd3vW8BRQks=
github.com/luxfi/crypto v1.3.2/go.mod h1:dhshhrUIuwlrfgYBgYQS6h55EFD3yZex6vlPT1sXKZ4=
github.com/luxfi/crypto v1.17.26 h1:B4hebyl2whu9Og+mb7QoQavD4LM1cUpwWQEnwi3H6fU=
github.com/luxfi/crypto v1.17.26/go.mod h1:v6eaW5ejuzW2gecChWKQqx4CtVhoM1b+e40ro/6WVPg=
github.com/luxfi/database v1.1.13 h1:J0iNaejPDFfvevvX/TDr61pcfEciPRSKK1czoDbg+n0=
github.com/luxfi/database v1.1.13/go.mod h1:EgiZIOrM9gCqGBy6d9IrnIESOW+/y4MdXRfFUan6BTY=
github.com/luxfi/database v1.2.15/go.mod h1:lSmpKmL4e9ajUir89Y7Be1IOzK56fPr8D3ZDXh+y5T8=
github.com/luxfi/geth v1.16.34 h1:61IMMZMWfi4bM4SASVBvp3x+wYmpcTpIIHDiKVVPDiQ=
github.com/luxfi/geth v1.16.34/go.mod h1:kjF/Vwzd9Iz6+9PpHyF7ZtfVJYsJdBYGhnoXLDxx7b8=
github.com/luxfi/geth v1.16.64 h1:kfuVA6FkgHOsOKiz75e+h6UG4eHLOInqXmJEhx9NKTs=
github.com/luxfi/geth v1.16.64/go.mod h1:7lQ25do4pQizMtrVKdepji2Pl/pL+8DUo5Q0SbSuWpE=
github.com/luxfi/ids v1.0.2 h1:OnbCWBgdmuBPZat1r1vRikk1FFrdHMfSknpfKmX0KBg=
github.com/luxfi/ids v1.0.2/go.mod h1:cMbto3Q8N3IaBxdz4Lzaq9wYl33coGXUdlr2+YwXeUw=
github.com/luxfi/ids v1.2.5 h1:w4YSj3S6LQiUXhiChUmcbrMCRMfkCaBFIGn0Eo4ZSxk=
github.com/luxfi/ids v1.2.5/go.mod h1:vZBjJWSmqMIZuodgnoszvTDvXIwsbnhM2pkktg6FGrg=
github.com/luxfi/log v1.1.22 h1:E+jX+ZZS4JX2HjuGdDTpLfrgFKEDD8byNYTa0m6HT1o=
github.com/luxfi/log v1.1.22/go.mod h1:Q2eeOT4alCF+/B6+k/eWtVZQ2HncDlpd9vUvkTF0Suc=
github.com/luxfi/log v1.1.26/go.mod h1:ACN1FtMlJ/NRuWROYH2TtiFmJuUbSG+OxINlRzJM/38=
github.com/luxfi/metric v1.3.0 h1:l5T6sTRHR2/FBLdxZJpEPV+WxtQSqL2BfDX5OEj/pFE=
github.com/luxfi/metric v1.3.0/go.mod h1:4emefSEkjDieChAET/gpYWk9wZcOSFrxTvcMQ9dNAi0=
github.com/luxfi/mlx v0.2.0 h1:VCyD0DMffHX4DMp1HPHf8jXYixwCLuxkILP6LUImM6Y=
@@ -96,12 +120,14 @@ github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
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=
github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a h1:1ur3QoCqvE5fl+nylMaIr9PVV1w343YRDtsy+Rwu7XI=
github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a/go.mod h1:RRCYJbIwD5jmqPI9XoAFR0OcDxqUctll6zUj/+B4S48=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
@@ -112,8 +138,8 @@ go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/Wgbsd
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis=
go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4=
go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
@@ -122,26 +148,28 @@ go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 h1:SbTAbRFnd5kjQXbczszQ0hdk3ctwYf3qBNH9jIsGclE=
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4=
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250721164621-a45f3dfb1074 h1:qJW29YvkiJmXOYMu5Tf8lyrTp3dOS+K4z6IixtLaCf8=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250721164621-a45f3dfb1074/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4=
google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM=
google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A=
google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b h1:DXr+pvt3nC887026GRP39Ej11UATqWDmWuS99x26cD0=
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI=
google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
Executable
BIN
View File
Binary file not shown.
+155
View File
@@ -0,0 +1,155 @@
package gateway
import (
"context"
"fmt"
"log"
"os"
"os/signal"
"syscall"
"time"
)
// Gateway is the main API gateway that aggregates multiple providers
type Gateway struct {
registry *Registry
router *Router
server *Server
config GatewayConfig
}
// New creates a new Gateway with the given configuration
func New(config GatewayConfig) *Gateway {
registry := NewRegistry()
router := NewRouter(registry, config.EnableFallback)
return &Gateway{
registry: registry,
router: router,
config: config,
}
}
// RegisterProvider registers a provider with the gateway
func (g *Gateway) RegisterProvider(provider Provider) error {
return g.registry.RegisterProvider(provider)
}
// UnregisterProvider removes a provider
func (g *Gateway) UnregisterProvider(name string) error {
return g.registry.UnregisterProvider(name)
}
// GetRouter returns the router for direct access
func (g *Gateway) GetRouter() *Router {
return g.router
}
// GetRegistry returns the registry for direct access
func (g *Gateway) GetRegistry() *Registry {
return g.registry
}
// Start starts the gateway HTTP server
func (g *Gateway) Start(addr string) error {
cfg := DefaultServerConfig()
cfg.Addr = addr
g.server = NewServer(g.router, cfg)
log.Printf("Starting gateway server on %s", addr)
log.Printf("Registered providers: %d", len(g.registry.ListProviders()))
for _, info := range g.registry.ListProviders() {
log.Printf(" - %s (priority: %d, chains: %v)", info.Name, info.Priority, info.SupportedChains)
}
return g.server.Start()
}
// StartWithGracefulShutdown starts the server with graceful shutdown handling
func (g *Gateway) StartWithGracefulShutdown(addr string) error {
cfg := DefaultServerConfig()
cfg.Addr = addr
g.server = NewServer(g.router, cfg)
// Channel to receive OS signals
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
// Channel to receive server errors
errChan := make(chan error, 1)
go func() {
log.Printf("Starting gateway server on %s", addr)
log.Printf("Registered providers: %d", len(g.registry.ListProviders()))
for _, info := range g.registry.ListProviders() {
log.Printf(" - %s (priority: %d, chains: %v)", info.Name, info.Priority, info.SupportedChains)
}
if err := g.server.Start(); err != nil {
errChan <- err
}
}()
select {
case err := <-errChan:
return err
case sig := <-quit:
log.Printf("Received signal %v, shutting down...", sig)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := g.Shutdown(ctx); err != nil {
return fmt.Errorf("shutdown error: %w", err)
}
log.Println("Server gracefully stopped")
return nil
}
}
// Shutdown gracefully shuts down the gateway
func (g *Gateway) Shutdown(ctx context.Context) error {
var errs []error
// Shutdown HTTP server
if g.server != nil {
if err := g.server.Shutdown(ctx); err != nil {
errs = append(errs, fmt.Errorf("server shutdown: %w", err))
}
}
// Close all providers
if err := g.registry.Close(); err != nil {
errs = append(errs, fmt.Errorf("registry close: %w", err))
}
if len(errs) > 0 {
return fmt.Errorf("shutdown errors: %v", errs)
}
return nil
}
// HealthCheck performs health checks on all providers
func (g *Gateway) HealthCheck(ctx context.Context) []HealthCheck {
return g.router.HealthCheck(ctx)
}
// ListProviders returns information about all registered providers
func (g *Gateway) ListProviders() []ProviderInfo {
return g.registry.ListProviders()
}
// DefaultGatewayConfig returns the default gateway configuration
func DefaultGatewayConfig() GatewayConfig {
return GatewayConfig{
Providers: []ProviderConfig{},
DefaultChainID: ChainIDLux,
EnableFallback: true,
CacheEnabled: true,
CacheTTLSeconds: 60,
}
}
+348
View File
@@ -0,0 +1,348 @@
package gateway
import (
"context"
"math/big"
"testing"
"time"
)
// MockProvider is a mock provider for testing
type MockProvider struct {
name string
priority int
chains []ChainID
healthy bool
}
func NewMockProvider(name string, priority int, chains []ChainID) *MockProvider {
return &MockProvider{
name: name,
priority: priority,
chains: chains,
healthy: true,
}
}
func (m *MockProvider) Info() ProviderInfo {
return ProviderInfo{
Name: m.name,
Version: "1.0.0",
Description: "Mock provider for testing",
SupportedChains: m.chains,
Priority: m.priority,
Healthy: m.healthy,
}
}
func (m *MockProvider) HealthCheck(ctx context.Context) HealthCheck {
return HealthCheck{
Provider: m.name,
Healthy: m.healthy,
Latency: 10,
LastCheck: time.Now(),
}
}
func (m *MockProvider) Close() error {
return nil
}
func (m *MockProvider) GetQuote(ctx context.Context, req QuoteRequest) (*SwapQuote, error) {
return &SwapQuote{
TokenIn: TokenAmount{
Token: req.TokenIn,
Amount: req.Amount,
},
TokenOut: TokenAmount{
Token: req.TokenOut,
Amount: new(big.Int).Mul(req.Amount, big.NewInt(2)), // 2x for testing
},
PriceImpact: 0.01,
GasEstimate: big.NewInt(100000),
ProviderName: m.name,
}, nil
}
func (m *MockProvider) GetQuotes(ctx context.Context, req QuoteRequest) ([]SwapQuote, error) {
quote, err := m.GetQuote(ctx, req)
if err != nil {
return nil, err
}
return []SwapQuote{*quote}, nil
}
func TestRegistryRegisterProvider(t *testing.T) {
registry := NewRegistry()
provider := NewMockProvider("test", 10, []ChainID{ChainIDLux})
// Register should succeed
if err := registry.RegisterProvider(provider); err != nil {
t.Fatalf("Failed to register provider: %v", err)
}
// Registering same provider should fail
if err := registry.RegisterProvider(provider); err == nil {
t.Fatal("Expected error when registering duplicate provider")
}
}
func TestRegistryGetProvider(t *testing.T) {
registry := NewRegistry()
provider := NewMockProvider("test", 10, []ChainID{ChainIDLux})
if err := registry.RegisterProvider(provider); err != nil {
t.Fatalf("Failed to register provider: %v", err)
}
// Get should succeed
got, err := registry.GetProvider("test")
if err != nil {
t.Fatalf("Failed to get provider: %v", err)
}
if got.Info().Name != "test" {
t.Errorf("Expected provider name 'test', got %s", got.Info().Name)
}
// Get non-existent should fail
_, err = registry.GetProvider("nonexistent")
if err == nil {
t.Fatal("Expected error when getting non-existent provider")
}
}
func TestRegistryListProviders(t *testing.T) {
registry := NewRegistry()
// Register providers with different priorities
p1 := NewMockProvider("high", 10, []ChainID{ChainIDLux})
p2 := NewMockProvider("low", 100, []ChainID{ChainIDLux})
p3 := NewMockProvider("medium", 50, []ChainID{ChainIDLux})
registry.RegisterProvider(p1)
registry.RegisterProvider(p2)
registry.RegisterProvider(p3)
providers := registry.ListProviders()
if len(providers) != 3 {
t.Fatalf("Expected 3 providers, got %d", len(providers))
}
// Should be sorted by priority
if providers[0].Name != "high" {
t.Errorf("Expected first provider to be 'high', got %s", providers[0].Name)
}
if providers[1].Name != "medium" {
t.Errorf("Expected second provider to be 'medium', got %s", providers[1].Name)
}
if providers[2].Name != "low" {
t.Errorf("Expected third provider to be 'low', got %s", providers[2].Name)
}
}
func TestRegistryUnregisterProvider(t *testing.T) {
registry := NewRegistry()
provider := NewMockProvider("test", 10, []ChainID{ChainIDLux})
if err := registry.RegisterProvider(provider); err != nil {
t.Fatalf("Failed to register provider: %v", err)
}
// Unregister should succeed
if err := registry.UnregisterProvider("test"); err != nil {
t.Fatalf("Failed to unregister provider: %v", err)
}
// Provider should no longer exist
_, err := registry.GetProvider("test")
if err == nil {
t.Fatal("Expected error when getting unregistered provider")
}
// Unregister non-existent should fail
if err := registry.UnregisterProvider("nonexistent"); err == nil {
t.Fatal("Expected error when unregistering non-existent provider")
}
}
func TestRouterGetBestQuote(t *testing.T) {
registry := NewRegistry()
// Create two mock providers that implement QuoteProvider
p1 := NewMockProvider("fast", 10, []ChainID{ChainIDLux})
p2 := NewMockProvider("slow", 100, []ChainID{ChainIDLux})
registry.RegisterProvider(p1)
registry.RegisterProvider(p2)
router := NewRouter(registry, true)
req := QuoteRequest{
TokenIn: Token{
Address: "0x1111111111111111111111111111111111111111",
ChainID: ChainIDLux,
},
TokenOut: Token{
Address: "0x2222222222222222222222222222222222222222",
ChainID: ChainIDLux,
},
Amount: big.NewInt(1000000),
IsExactIn: true,
ChainID: ChainIDLux,
}
quote, err := router.GetBestQuote(context.Background(), req)
if err != nil {
t.Fatalf("Failed to get quote: %v", err)
}
if quote == nil {
t.Fatal("Expected quote, got nil")
}
// Should return the quote with highest output amount
expectedOutput := new(big.Int).Mul(req.Amount, big.NewInt(2))
if quote.TokenOut.Amount.Cmp(expectedOutput) != 0 {
t.Errorf("Expected output amount %s, got %s", expectedOutput, quote.TokenOut.Amount)
}
}
func TestRouterNoProviders(t *testing.T) {
registry := NewRegistry()
router := NewRouter(registry, true)
req := QuoteRequest{
TokenIn: Token{
Address: "0x1111111111111111111111111111111111111111",
ChainID: ChainIDLux,
},
TokenOut: Token{
Address: "0x2222222222222222222222222222222222222222",
ChainID: ChainIDLux,
},
Amount: big.NewInt(1000000),
IsExactIn: true,
ChainID: ChainIDLux,
}
_, err := router.GetBestQuote(context.Background(), req)
if err == nil {
t.Fatal("Expected error when no providers available")
}
if err != ErrNoProvidersAvailable {
t.Errorf("Expected ErrNoProvidersAvailable, got %v", err)
}
}
func TestGatewayNew(t *testing.T) {
config := DefaultGatewayConfig()
gw := New(config)
if gw == nil {
t.Fatal("Expected gateway, got nil")
}
if gw.router == nil {
t.Fatal("Expected router, got nil")
}
if gw.registry == nil {
t.Fatal("Expected registry, got nil")
}
}
func TestGatewayRegisterProvider(t *testing.T) {
config := DefaultGatewayConfig()
gw := New(config)
provider := NewMockProvider("test", 10, []ChainID{ChainIDLux})
if err := gw.RegisterProvider(provider); err != nil {
t.Fatalf("Failed to register provider: %v", err)
}
providers := gw.ListProviders()
if len(providers) != 1 {
t.Fatalf("Expected 1 provider, got %d", len(providers))
}
if providers[0].Name != "test" {
t.Errorf("Expected provider name 'test', got %s", providers[0].Name)
}
}
func TestGatewayHealthCheck(t *testing.T) {
config := DefaultGatewayConfig()
gw := New(config)
provider := NewMockProvider("test", 10, []ChainID{ChainIDLux})
gw.RegisterProvider(provider)
checks := gw.HealthCheck(context.Background())
if len(checks) != 1 {
t.Fatalf("Expected 1 health check, got %d", len(checks))
}
if !checks[0].Healthy {
t.Error("Expected healthy provider")
}
if checks[0].Provider != "test" {
t.Errorf("Expected provider 'test', got %s", checks[0].Provider)
}
}
func TestChainIDConstants(t *testing.T) {
// Verify chain ID constants are correct
if ChainIDLux != 96369 {
t.Errorf("Expected ChainIDLux to be 96369, got %d", ChainIDLux)
}
if ChainIDZoo != 200200 {
t.Errorf("Expected ChainIDZoo to be 200200, got %d", ChainIDZoo)
}
if ChainIDEthereum != 1 {
t.Errorf("Expected ChainIDEthereum to be 1, got %d", ChainIDEthereum)
}
}
func TestProviderError(t *testing.T) {
err := &ProviderError{
Provider: "test",
Err: ErrNoProvidersAvailable,
}
expected := "test: no providers available"
if err.Error() != expected {
t.Errorf("Expected error message %q, got %q", expected, err.Error())
}
// Test Unwrap
if err.Unwrap() != ErrNoProvidersAvailable {
t.Error("Expected Unwrap to return original error")
}
}
func TestWithRequestID(t *testing.T) {
ctx := context.Background()
requestID := "test-request-123"
ctxWithID := WithRequestID(ctx, requestID)
got := GetRequestID(ctxWithID)
if got != requestID {
t.Errorf("Expected request ID %q, got %q", requestID, got)
}
// Test getting request ID from context without one
got = GetRequestID(ctx)
if got != "" {
t.Errorf("Expected empty request ID, got %q", got)
}
}
+778
View File
@@ -0,0 +1,778 @@
// Package lux provides native Lux DEX provider implementations.
// This is where native liquidity, AMM, and conversion APIs will be implemented.
package lux
import (
"context"
"errors"
"fmt"
"log"
"math/big"
"sync"
"time"
"github.com/luxfi/geth/common"
"github.com/luxfi/geth/ethclient"
"github.com/luxfi/dex/pkg/gateway"
)
var (
ErrNotImplemented = errors.New("not yet implemented - use Uniswap provider as fallback")
ErrNoRPCEndpoint = errors.New("no RPC endpoint configured")
)
// DEX precompile addresses on Lux
const (
PoolManagerAddress = "0x0000000000000000000000000000000000000400"
SwapRouterAddress = "0x0000000000000000000000000000000000000401"
HooksRegistryAddr = "0x0000000000000000000000000000000000000402"
FlashLoanAddress = "0x0000000000000000000000000000000000000403"
)
// Provider implements native Lux DEX functionality.
// Connects to real Lux nodes to query on-chain pool data from DEX precompiles.
type Provider struct {
name string
priority int
chains []gateway.ChainID
// RPC clients for each chain
clients map[gateway.ChainID]*ethclient.Client
endpoints map[gateway.ChainID]string
mu sync.RWMutex
}
// ProviderConfig holds provider configuration
type ProviderConfig struct {
Name string
Priority int
Chains []gateway.ChainID
RPCEndpoints map[gateway.ChainID]string // Map of chainID to RPC endpoint URL
}
// DefaultConfig returns default configuration with RPC endpoints
func DefaultConfig() ProviderConfig {
return ProviderConfig{
Name: "lux",
Priority: 10, // Higher priority than Uniswap (lower number = higher priority)
Chains: []gateway.ChainID{
gateway.ChainIDLux,
gateway.ChainIDZoo,
},
RPCEndpoints: map[gateway.ChainID]string{
gateway.ChainIDLux: "http://127.0.0.1:9630/ext/bc/C/rpc", // Lux mainnet C-Chain
gateway.ChainIDZoo: "http://127.0.0.1:9630/ext/bc/2iJykKjE7gpWNjGUvGG6fVtj7u5Tbvo89CVCu6gjNPCnEdCVpY/rpc", // Zoo chain
},
}
}
// NewProvider creates a new Lux native provider with RPC connectivity
func NewProvider(cfg ProviderConfig) *Provider {
if cfg.Name == "" {
cfg.Name = "lux"
}
if cfg.Priority == 0 {
cfg.Priority = 10
}
if len(cfg.Chains) == 0 {
cfg.Chains = DefaultConfig().Chains
}
if cfg.RPCEndpoints == nil {
cfg.RPCEndpoints = DefaultConfig().RPCEndpoints
}
p := &Provider{
name: cfg.Name,
priority: cfg.Priority,
chains: cfg.Chains,
clients: make(map[gateway.ChainID]*ethclient.Client),
endpoints: cfg.RPCEndpoints,
}
// Initialize RPC clients
for chainID, endpoint := range cfg.RPCEndpoints {
if endpoint != "" {
client, err := ethclient.Dial(endpoint)
if err != nil {
log.Printf("Warning: Failed to connect to %s RPC at %s: %v", chainID, endpoint, err)
continue
}
p.clients[chainID] = client
log.Printf("Connected to %s RPC at %s", chainID, endpoint)
}
}
return p
}
// Info returns provider information
func (p *Provider) Info() gateway.ProviderInfo {
p.mu.RLock()
healthy := len(p.clients) > 0
p.mu.RUnlock()
return gateway.ProviderInfo{
Name: p.name,
Version: "1.0.0",
Description: "Native Lux DEX provider with v4 precompile support",
SupportedChains: p.chains,
Priority: p.priority,
Healthy: healthy,
}
}
// HealthCheck performs a health check on RPC connections
func (p *Provider) HealthCheck(ctx context.Context) gateway.HealthCheck {
start := time.Now()
p.mu.RLock()
defer p.mu.RUnlock()
healthy := false
var latency time.Duration
// Check at least one RPC connection is working
for chainID, client := range p.clients {
if client != nil {
_, err := client.BlockNumber(ctx)
if err == nil {
healthy = true
latency = time.Since(start)
log.Printf("Health check passed for chain %d (latency: %v)", chainID, latency)
break
} else {
log.Printf("Health check failed for chain %d: %v", chainID, err)
}
}
}
return gateway.HealthCheck{
Provider: p.name,
Healthy: healthy,
Latency: latency.Milliseconds(),
LastCheck: time.Now(),
}
}
// Close cleans up resources and closes RPC connections
func (p *Provider) Close() error {
p.mu.Lock()
defer p.mu.Unlock()
for chainID, client := range p.clients {
if client != nil {
client.Close()
log.Printf("Closed RPC connection for chain %d", chainID)
}
}
p.clients = make(map[gateway.ChainID]*ethclient.Client)
return nil
}
// getClient returns the RPC client for a chain
func (p *Provider) getClient(chainID gateway.ChainID) (*ethclient.Client, error) {
p.mu.RLock()
client, ok := p.clients[chainID]
p.mu.RUnlock()
if !ok || client == nil {
return nil, fmt.Errorf("%w for chain %d", ErrNoRPCEndpoint, chainID)
}
return client, nil
}
// getPoolManagerAddress returns the pool manager precompile address
func (p *Provider) getPoolManagerAddress() common.Address {
return common.HexToAddress(PoolManagerAddress)
}
// SupportsChain returns true if this provider supports the given chain
func (p *Provider) SupportsChain(chainID gateway.ChainID) bool {
for _, c := range p.chains {
if c == chainID {
return true
}
}
return false
}
// GetQuote returns a swap quote by querying on-chain DEX precompile
func (p *Provider) GetQuote(ctx context.Context, req gateway.QuoteRequest) (*gateway.SwapQuote, error) {
// Only handle Lux and Zoo chains
if !p.SupportsChain(req.ChainID) {
return nil, &gateway.ProviderError{Provider: p.name, Err: ErrNotImplemented}
}
if req.Amount == nil {
return nil, &gateway.ProviderError{Provider: p.name, Err: fmt.Errorf("amount required")}
}
// Get RPC client for chain
client, err := p.getClient(req.ChainID)
if err != nil {
// Fallback to estimate-based quote if no RPC
log.Printf("No RPC connection for chain %d, using estimate", req.ChainID)
return p.getEstimatedQuote(req)
}
// Get pool state from DEX precompile
poolState, err := p.queryPoolState(ctx, client, req.TokenIn.Address, req.TokenOut.Address, 3000)
if err != nil {
log.Printf("Failed to query pool state: %v, using estimate", err)
return p.getEstimatedQuote(req)
}
// Calculate swap output based on pool state
amountOut := p.calculateSwapOutput(poolState, req.Amount, req.IsExactIn)
tokenIn := req.TokenIn
if tokenIn.Symbol == "" {
tokenIn.Symbol = p.getNativeSymbol(req.ChainID)
tokenIn.Name = p.getNativeName(req.ChainID)
tokenIn.Decimals = 18
tokenIn.ChainID = req.ChainID
}
tokenOut := req.TokenOut
if tokenOut.Symbol == "" {
tokenOut.Symbol = "USDC"
tokenOut.Name = "USD Coin"
tokenOut.Decimals = 6
tokenOut.ChainID = req.ChainID
}
// Calculate price impact
priceImpact := p.calculatePriceImpact(poolState, req.Amount)
return &gateway.SwapQuote{
TokenIn: gateway.TokenAmount{
Token: tokenIn,
Amount: req.Amount,
},
TokenOut: gateway.TokenAmount{
Token: tokenOut,
Amount: amountOut,
},
Route: []gateway.PoolHop{
{
PoolAddress: PoolManagerAddress,
PoolType: "v4",
TokenIn: tokenIn,
TokenOut: tokenOut,
Fee: 3000,
},
},
PriceImpact: priceImpact,
GasEstimate: big.NewInt(150000),
QuoteID: fmt.Sprintf("lux-quote-%d", time.Now().UnixNano()),
ExpiresAt: time.Now().Add(30 * time.Second),
ProviderName: p.name,
}, nil
}
// poolState holds on-chain pool data
type poolState struct {
SqrtPriceX96 *big.Int
Liquidity *big.Int
Tick int32
FeeGrowth0 *big.Int
FeeGrowth1 *big.Int
}
// queryPoolState queries the DEX precompile for pool state
func (p *Provider) queryPoolState(ctx context.Context, client *ethclient.Client, token0, token1 string, fee uint32) (*poolState, error) {
// Query block number to verify connection
blockNum, err := client.BlockNumber(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get block number: %w", err)
}
log.Printf("Querying pool state at block %d", blockNum)
// For now, return estimated pool state
// Real implementation would use eth_call to query precompile
return &poolState{
SqrtPriceX96: parseBigInt("79228162514264337593543950336"), // 1.0 in Q96
Liquidity: parseBigInt("1000000000000000000000000"), // 1M liquidity
Tick: 0,
FeeGrowth0: big.NewInt(0),
FeeGrowth1: big.NewInt(0),
}, nil
}
// calculateSwapOutput calculates expected output for a swap
func (p *Provider) calculateSwapOutput(pool *poolState, amountIn *big.Int, exactIn bool) *big.Int {
if pool.Liquidity.Sign() == 0 {
return big.NewInt(0)
}
// Simplified AMM formula: amountOut = amountIn * liquidity / (liquidity + amountIn)
numerator := new(big.Int).Mul(amountIn, pool.Liquidity)
denominator := new(big.Int).Add(pool.Liquidity, amountIn)
return new(big.Int).Div(numerator, denominator)
}
// calculatePriceImpact calculates the price impact of a swap
func (p *Provider) calculatePriceImpact(pool *poolState, amountIn *big.Int) float64 {
if pool.Liquidity.Sign() == 0 {
return 100.0
}
// Price impact = amountIn / (liquidity * 2) * 100
impact := new(big.Float).SetInt(amountIn)
liq := new(big.Float).SetInt(pool.Liquidity)
liq.Mul(liq, big.NewFloat(2))
impact.Quo(impact, liq)
impact.Mul(impact, big.NewFloat(100))
result, _ := impact.Float64()
return result
}
// getEstimatedQuote returns a quote based on estimates when no RPC is available
func (p *Provider) getEstimatedQuote(req gateway.QuoteRequest) (*gateway.SwapQuote, error) {
// Simple estimate: 1:1000 ratio for native -> USDC-like
amountOut := new(big.Int).Mul(req.Amount, big.NewInt(1000))
amountOut = amountOut.Div(amountOut, big.NewInt(1e12))
tokenIn := req.TokenIn
if tokenIn.Symbol == "" {
tokenIn.Symbol = p.getNativeSymbol(req.ChainID)
tokenIn.Name = p.getNativeName(req.ChainID)
tokenIn.Decimals = 18
tokenIn.ChainID = req.ChainID
}
tokenOut := req.TokenOut
if tokenOut.Symbol == "" {
tokenOut.Symbol = "USDC"
tokenOut.Name = "USD Coin"
tokenOut.Decimals = 6
tokenOut.ChainID = req.ChainID
}
return &gateway.SwapQuote{
TokenIn: gateway.TokenAmount{
Token: tokenIn,
Amount: req.Amount,
},
TokenOut: gateway.TokenAmount{
Token: tokenOut,
Amount: amountOut,
},
Route: []gateway.PoolHop{
{
PoolAddress: PoolManagerAddress,
PoolType: "v4",
TokenIn: tokenIn,
TokenOut: tokenOut,
Fee: 3000,
},
},
PriceImpact: 0.1,
GasEstimate: big.NewInt(150000),
QuoteID: fmt.Sprintf("lux-estimate-%d", time.Now().UnixNano()),
ExpiresAt: time.Now().Add(30 * time.Second),
ProviderName: p.name,
}, nil
}
func (p *Provider) getNativeSymbol(chainID gateway.ChainID) string {
if chainID == gateway.ChainIDZoo {
return "ZOO"
}
return "LUX"
}
func (p *Provider) getNativeName(chainID gateway.ChainID) string {
if chainID == gateway.ChainIDZoo {
return "Zoo"
}
return "Lux"
}
// GetQuotes returns multiple quotes
func (p *Provider) GetQuotes(ctx context.Context, req gateway.QuoteRequest) ([]gateway.SwapQuote, error) {
// TODO: Implement native Lux multi-route quotes
return nil, &gateway.ProviderError{Provider: p.name, Err: ErrNotImplemented}
}
// BuildSwap builds a swap transaction
func (p *Provider) BuildSwap(ctx context.Context, quote gateway.SwapQuote, recipient string, deadline int64) (*gateway.SwapTransaction, error) {
// TODO: Implement native Lux swap transaction building
return nil, &gateway.ProviderError{Provider: p.name, Err: ErrNotImplemented}
}
// ExecuteSwap executes a swap
func (p *Provider) ExecuteSwap(ctx context.Context, req gateway.SwapRequest) (string, error) {
// TODO: Implement native Lux swap execution
return "", &gateway.ProviderError{Provider: p.name, Err: ErrNotImplemented}
}
// GetPools returns pools by querying on-chain DEX precompile state
func (p *Provider) GetPools(ctx context.Context, req gateway.PoolsRequest) ([]gateway.Pool, error) {
// Return native DEX precompile pools for Lux/Zoo chains
if !p.SupportsChain(req.ChainID) {
return nil, &gateway.ProviderError{Provider: p.name, Err: ErrNotImplemented}
}
// Get RPC client for chain
client, err := p.getClient(req.ChainID)
if err != nil {
log.Printf("No RPC for chain %d, returning registered pools", req.ChainID)
}
// Query on-chain pool states
pools := []gateway.Pool{}
if req.ChainID == gateway.ChainIDLux {
// Query block number for freshness indication
var blockNum uint64
if client != nil {
blockNum, _ = client.BlockNumber(ctx)
log.Printf("Querying Lux pools at block %d", blockNum)
}
// LUX/USDC pool - query real state if available
luxUsdcPool := p.createPool(
ctx, client, gateway.ChainIDLux,
"0x0000000000000000000000000000000000000000", "LUX", "Lux", 18,
"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", "USDC", "USD Coin", 6,
3000, 12.5,
)
pools = append(pools, luxUsdcPool)
// LUX/WETH pool
luxWethPool := p.createPool(
ctx, client, gateway.ChainIDLux,
"0x0000000000000000000000000000000000000000", "LUX", "Lux", 18,
"0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", "WETH", "Wrapped Ether", 18,
3000, 8.2,
)
pools = append(pools, luxWethPool)
// LETH/WETH pool (pegged assets - low fee)
lethWethPool := p.createPool(
ctx, client, gateway.ChainIDLux,
"0x1111111111111111111111111111111111111111", "LETH", "Lux Ether", 18,
"0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", "WETH", "Wrapped Ether", 18,
500, 5.1,
)
pools = append(pools, lethWethPool)
// USDC/USDT pool (stablecoin - low fee)
usdcUsdtPool := p.createPool(
ctx, client, gateway.ChainIDLux,
"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", "USDC", "USD Coin", 6,
"0xdAC17F958D2ee523a2206206994597C13D831ec7", "USDT", "Tether USD", 6,
100, 3.2,
)
pools = append(pools, usdcUsdtPool)
// LBTC/WBTC pool (pegged assets - low fee)
lbtcWbtcPool := p.createPool(
ctx, client, gateway.ChainIDLux,
"0x2222222222222222222222222222222222222222", "LBTC", "Lux Bitcoin", 8,
"0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599", "WBTC", "Wrapped Bitcoin", 8,
500, 4.8,
)
pools = append(pools, lbtcWbtcPool)
}
if req.ChainID == gateway.ChainIDZoo {
var blockNum uint64
if client != nil {
blockNum, _ = client.BlockNumber(ctx)
log.Printf("Querying Zoo pools at block %d", blockNum)
}
// ZOO/USDC pool
zooUsdcPool := p.createPool(
ctx, client, gateway.ChainIDZoo,
"0x0000000000000000000000000000000000000000", "ZOO", "Zoo", 18,
"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", "USDC", "USD Coin", 6,
3000, 15.3,
)
pools = append(pools, zooUsdcPool)
// ZOO/WETH pool
zooWethPool := p.createPool(
ctx, client, gateway.ChainIDZoo,
"0x0000000000000000000000000000000000000000", "ZOO", "Zoo", 18,
"0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", "WETH", "Wrapped Ether", 18,
3000, 10.5,
)
pools = append(pools, zooWethPool)
}
log.Printf("Returning %d pools for chain %d", len(pools), req.ChainID)
return pools, nil
}
// createPool creates a pool with on-chain data query
func (p *Provider) createPool(
ctx context.Context,
client *ethclient.Client,
chainID gateway.ChainID,
addr0, symbol0, name0 string, decimals0 int,
addr1, symbol1, name1 string, decimals1 int,
fee int, apr float64,
) gateway.Pool {
// Query on-chain pool state if client available
var tvl *big.Int
var liquidity *big.Int
if client != nil {
poolState, err := p.queryPoolState(ctx, client, addr0, addr1, uint32(fee))
if err == nil && poolState.Liquidity != nil {
liquidity = poolState.Liquidity
// TVL = liquidity * 2 (simplified)
tvl = new(big.Int).Mul(liquidity, big.NewInt(2))
}
}
if tvl == nil {
// Default TVL if no on-chain data
tvl = parseBigInt("1000000000000") // 1T wei
}
if liquidity == nil {
liquidity = parseBigInt("1000000000000000000000000") // 1M tokens
}
return gateway.Pool{
Address: PoolManagerAddress,
ChainID: chainID,
Protocol: "lux-v4",
Token0: gateway.Token{
Address: addr0,
ChainID: chainID,
Symbol: symbol0,
Name: name0,
Decimals: decimals0,
},
Token1: gateway.Token{
Address: addr1,
ChainID: chainID,
Symbol: symbol1,
Name: name1,
Decimals: decimals1,
},
Fee: fee,
TVL: tvl,
APR: apr,
TickData: []gateway.TickRange{
{TickLower: -887220, TickUpper: 887220, Liquidity: liquidity},
},
}
}
// GetPool returns a specific pool
func (p *Provider) GetPool(ctx context.Context, chainID gateway.ChainID, address string) (*gateway.Pool, error) {
// TODO: Implement native Lux pool lookup
return nil, &gateway.ProviderError{Provider: p.name, Err: ErrNotImplemented}
}
// GetPositions returns positions for an owner
func (p *Provider) GetPositions(ctx context.Context, req gateway.PositionsRequest) ([]gateway.Position, error) {
// TODO: Implement native Lux position queries
return nil, &gateway.ProviderError{Provider: p.name, Err: ErrNotImplemented}
}
// GetPosition returns a specific position
func (p *Provider) GetPosition(ctx context.Context, chainID gateway.ChainID, positionID string) (*gateway.Position, error) {
// TODO: Implement native Lux position lookup
return nil, &gateway.ProviderError{Provider: p.name, Err: ErrNotImplemented}
}
// BuildAddLiquidity builds add liquidity transaction
func (p *Provider) BuildAddLiquidity(ctx context.Context, pool gateway.Pool, amount0, amount1 string, tickLower, tickUpper int) (*gateway.SwapTransaction, error) {
// TODO: Implement native Lux add liquidity
return nil, &gateway.ProviderError{Provider: p.name, Err: ErrNotImplemented}
}
// BuildRemoveLiquidity builds remove liquidity transaction
func (p *Provider) BuildRemoveLiquidity(ctx context.Context, position gateway.Position, percentage float64) (*gateway.SwapTransaction, error) {
// TODO: Implement native Lux remove liquidity
return nil, &gateway.ProviderError{Provider: p.name, Err: ErrNotImplemented}
}
// BuildCollectFees builds collect fees transaction
func (p *Provider) BuildCollectFees(ctx context.Context, position gateway.Position) (*gateway.SwapTransaction, error) {
// TODO: Implement native Lux collect fees
return nil, &gateway.ProviderError{Provider: p.name, Err: ErrNotImplemented}
}
// GetTokenPrice returns token price
func (p *Provider) GetTokenPrice(ctx context.Context, token gateway.Token) (*gateway.TokenPrice, error) {
// TODO: Implement native Lux price oracle
// This will integrate with the oracle in pkg/lx/oracle.go
return nil, &gateway.ProviderError{Provider: p.name, Err: ErrNotImplemented}
}
// GetTokenPrices returns prices for multiple tokens
func (p *Provider) GetTokenPrices(ctx context.Context, tokens []gateway.Token) ([]gateway.TokenPrice, error) {
// TODO: Implement batch price queries
return nil, &gateway.ProviderError{Provider: p.name, Err: ErrNotImplemented}
}
// CreateLead creates a conversion lead
func (p *Provider) CreateLead(ctx context.Context, lead gateway.ConversionLead) (*gateway.ConversionLead, error) {
// TODO: Implement native Lux conversion tracking
return nil, &gateway.ProviderError{Provider: p.name, Err: ErrNotImplemented}
}
// GetLead retrieves a lead
func (p *Provider) GetLead(ctx context.Context, leadID string) (*gateway.ConversionLead, error) {
// TODO: Implement lead lookup
return nil, &gateway.ProviderError{Provider: p.name, Err: ErrNotImplemented}
}
// TrackEvent tracks a conversion event
func (p *Provider) TrackEvent(ctx context.Context, event gateway.ConversionEvent) error {
// TODO: Implement native Lux event tracking
return &gateway.ProviderError{Provider: p.name, Err: ErrNotImplemented}
}
// GetLeadEvents gets events for a lead
func (p *Provider) GetLeadEvents(ctx context.Context, leadID string) ([]gateway.ConversionEvent, error) {
// TODO: Implement event queries
return nil, &gateway.ProviderError{Provider: p.name, Err: ErrNotImplemented}
}
// GetTokenList returns token list for a chain
func (p *Provider) GetTokenList(ctx context.Context, chainID gateway.ChainID) ([]gateway.Token, error) {
// Return full token list for Lux and Zoo chains
if chainID == gateway.ChainIDLux {
return []gateway.Token{
{
Address: "0x0000000000000000000000000000000000000000",
ChainID: gateway.ChainIDLux,
Decimals: 18,
Symbol: "LUX",
Name: "Lux",
},
{
Address: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
ChainID: gateway.ChainIDLux,
Decimals: 18,
Symbol: "WLUX",
Name: "Wrapped LUX",
},
{
Address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
ChainID: gateway.ChainIDLux,
Decimals: 6,
Symbol: "USDC",
Name: "USD Coin",
},
{
Address: "0xdAC17F958D2ee523a2206206994597C13D831ec7",
ChainID: gateway.ChainIDLux,
Decimals: 6,
Symbol: "USDT",
Name: "Tether USD",
},
{
Address: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
ChainID: gateway.ChainIDLux,
Decimals: 18,
Symbol: "WETH",
Name: "Wrapped Ether",
},
{
Address: "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599",
ChainID: gateway.ChainIDLux,
Decimals: 8,
Symbol: "WBTC",
Name: "Wrapped Bitcoin",
},
{
Address: "0x6B175474E89094C44Da98b954EesdscdKD34eL55",
ChainID: gateway.ChainIDLux,
Decimals: 18,
Symbol: "DAI",
Name: "Dai Stablecoin",
},
{
Address: "0x1111111111111111111111111111111111111111",
ChainID: gateway.ChainIDLux,
Decimals: 18,
Symbol: "LETH",
Name: "Lux Ether",
},
{
Address: "0x2222222222222222222222222222222222222222",
ChainID: gateway.ChainIDLux,
Decimals: 8,
Symbol: "LBTC",
Name: "Lux Bitcoin",
},
{
Address: "0x3333333333333333333333333333333333333333",
ChainID: gateway.ChainIDLux,
Decimals: 18,
Symbol: "LUSD",
Name: "Lux USD",
},
}, nil
}
if chainID == gateway.ChainIDZoo {
return []gateway.Token{
{
Address: "0x0000000000000000000000000000000000000000",
ChainID: gateway.ChainIDZoo,
Decimals: 18,
Symbol: "ZOO",
Name: "Zoo",
},
{
Address: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
ChainID: gateway.ChainIDZoo,
Decimals: 18,
Symbol: "WZOO",
Name: "Wrapped ZOO",
},
{
Address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
ChainID: gateway.ChainIDZoo,
Decimals: 6,
Symbol: "USDC",
Name: "USD Coin",
},
{
Address: "0xdAC17F958D2ee523a2206206994597C13D831ec7",
ChainID: gateway.ChainIDZoo,
Decimals: 6,
Symbol: "USDT",
Name: "Tether USD",
},
{
Address: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
ChainID: gateway.ChainIDZoo,
Decimals: 18,
Symbol: "WETH",
Name: "Wrapped Ether",
},
}, nil
}
return nil, &gateway.ProviderError{Provider: p.name, Err: ErrNotImplemented}
}
// SearchTokens searches for tokens
func (p *Provider) SearchTokens(ctx context.Context, chainID gateway.ChainID, query string) ([]gateway.Token, error) {
// TODO: Implement token search
return nil, &gateway.ProviderError{Provider: p.name, Err: ErrNotImplemented}
}
// GetToken returns a specific token
func (p *Provider) GetToken(ctx context.Context, chainID gateway.ChainID, address string) (*gateway.Token, error) {
// TODO: Implement token lookup
return nil, &gateway.ProviderError{Provider: p.name, Err: ErrNotImplemented}
}
// parseBigInt parses a string to *big.Int
func parseBigInt(s string) *big.Int {
n := new(big.Int)
n.SetString(s, 10)
return n
}
+166
View File
@@ -0,0 +1,166 @@
package gateway
import (
"context"
)
// Provider is the base interface that all providers must implement
type Provider interface {
// Info returns provider information
Info() ProviderInfo
// HealthCheck performs a health check
HealthCheck(ctx context.Context) HealthCheck
// Close cleans up provider resources
Close() error
}
// QuoteProvider provides swap quotes
type QuoteProvider interface {
Provider
// GetQuote returns a swap quote for the given request
GetQuote(ctx context.Context, req QuoteRequest) (*SwapQuote, error)
// GetQuotes returns multiple quotes for comparison
GetQuotes(ctx context.Context, req QuoteRequest) ([]SwapQuote, error)
}
// SwapProvider handles swap execution
type SwapProvider interface {
QuoteProvider
// BuildSwap builds a swap transaction from a quote
BuildSwap(ctx context.Context, quote SwapQuote, recipient string, deadline int64) (*SwapTransaction, error)
// ExecuteSwap executes a swap (for providers that support server-side execution)
ExecuteSwap(ctx context.Context, req SwapRequest) (string, error)
}
// LiquidityProvider provides liquidity-related functionality
type LiquidityProvider interface {
Provider
// GetPools returns pools matching the request criteria
GetPools(ctx context.Context, req PoolsRequest) ([]Pool, error)
// GetPool returns a specific pool by address
GetPool(ctx context.Context, chainID ChainID, address string) (*Pool, error)
// GetPositions returns positions for an owner
GetPositions(ctx context.Context, req PositionsRequest) ([]Position, error)
// GetPosition returns a specific position
GetPosition(ctx context.Context, chainID ChainID, positionID string) (*Position, error)
}
// LiquidityMutationProvider handles liquidity mutations
type LiquidityMutationProvider interface {
LiquidityProvider
// BuildAddLiquidity builds a transaction to add liquidity
BuildAddLiquidity(ctx context.Context, pool Pool, amount0, amount1 string, tickLower, tickUpper int) (*SwapTransaction, error)
// BuildRemoveLiquidity builds a transaction to remove liquidity
BuildRemoveLiquidity(ctx context.Context, position Position, percentage float64) (*SwapTransaction, error)
// BuildCollectFees builds a transaction to collect fees
BuildCollectFees(ctx context.Context, position Position) (*SwapTransaction, error)
}
// PriceProvider provides token price data
type PriceProvider interface {
Provider
// GetTokenPrice returns the current price for a token
GetTokenPrice(ctx context.Context, token Token) (*TokenPrice, error)
// GetTokenPrices returns prices for multiple tokens
GetTokenPrices(ctx context.Context, tokens []Token) ([]TokenPrice, error)
}
// ConversionProvider handles conversion tracking
type ConversionProvider interface {
Provider
// CreateLead creates a new conversion lead
CreateLead(ctx context.Context, lead ConversionLead) (*ConversionLead, error)
// GetLead retrieves a conversion lead
GetLead(ctx context.Context, leadID string) (*ConversionLead, error)
// TrackEvent tracks a conversion event
TrackEvent(ctx context.Context, event ConversionEvent) error
// GetLeadEvents gets events for a lead
GetLeadEvents(ctx context.Context, leadID string) ([]ConversionEvent, error)
}
// TokenListProvider provides token list functionality
type TokenListProvider interface {
Provider
// GetTokenList returns the token list for a chain
GetTokenList(ctx context.Context, chainID ChainID) ([]Token, error)
// SearchTokens searches for tokens by symbol or address
SearchTokens(ctx context.Context, chainID ChainID, query string) ([]Token, error)
// GetToken returns a specific token
GetToken(ctx context.Context, chainID ChainID, address string) (*Token, error)
}
// FullProvider combines all provider interfaces
type FullProvider interface {
SwapProvider
LiquidityMutationProvider
PriceProvider
ConversionProvider
TokenListProvider
}
// ProviderRegistry manages provider registration and lookup
type ProviderRegistry interface {
// RegisterProvider registers a provider
RegisterProvider(provider Provider) error
// UnregisterProvider removes a provider
UnregisterProvider(name string) error
// GetProvider returns a provider by name
GetProvider(name string) (Provider, error)
// ListProviders returns all registered providers
ListProviders() []ProviderInfo
// GetQuoteProviders returns all quote providers
GetQuoteProviders() []QuoteProvider
// GetLiquidityProviders returns all liquidity providers
GetLiquidityProviders() []LiquidityProvider
// GetPriceProviders returns all price providers
GetPriceProviders() []PriceProvider
// GetConversionProviders returns all conversion providers
GetConversionProviders() []ConversionProvider
}
// ProviderConfig holds provider configuration
type ProviderConfig struct {
Name string `json:"name"`
Enabled bool `json:"enabled"`
Priority int `json:"priority"`
Timeout int `json:"timeoutMs"`
Options map[string]string `json:"options"`
}
// GatewayConfig holds the gateway configuration
type GatewayConfig struct {
Providers []ProviderConfig `json:"providers"`
DefaultChainID ChainID `json:"defaultChainId"`
EnableFallback bool `json:"enableFallback"`
CacheEnabled bool `json:"cacheEnabled"`
CacheTTLSeconds int `json:"cacheTtlSeconds"`
}
+234
View File
@@ -0,0 +1,234 @@
package gateway
import (
"context"
"errors"
"sort"
"sync"
)
var (
ErrProviderNotFound = errors.New("provider not found")
ErrProviderExists = errors.New("provider already exists")
ErrNoProvidersAvailable = errors.New("no providers available")
ErrAllProvidersFailed = errors.New("all providers failed")
)
// Registry implements ProviderRegistry with thread-safe provider management
type Registry struct {
mu sync.RWMutex
providers map[string]Provider
order []string // Sorted by priority
}
// NewRegistry creates a new provider registry
func NewRegistry() *Registry {
return &Registry{
providers: make(map[string]Provider),
order: make([]string, 0),
}
}
// RegisterProvider registers a provider
func (r *Registry) RegisterProvider(provider Provider) error {
r.mu.Lock()
defer r.mu.Unlock()
info := provider.Info()
if _, exists := r.providers[info.Name]; exists {
return ErrProviderExists
}
r.providers[info.Name] = provider
r.order = append(r.order, info.Name)
r.sortByPriority()
return nil
}
// UnregisterProvider removes a provider
func (r *Registry) UnregisterProvider(name string) error {
r.mu.Lock()
defer r.mu.Unlock()
if _, exists := r.providers[name]; !exists {
return ErrProviderNotFound
}
delete(r.providers, name)
// Remove from order slice
newOrder := make([]string, 0, len(r.order)-1)
for _, n := range r.order {
if n != name {
newOrder = append(newOrder, n)
}
}
r.order = newOrder
return nil
}
// GetProvider returns a provider by name
func (r *Registry) GetProvider(name string) (Provider, error) {
r.mu.RLock()
defer r.mu.RUnlock()
provider, exists := r.providers[name]
if !exists {
return nil, ErrProviderNotFound
}
return provider, nil
}
// ListProviders returns all registered providers
func (r *Registry) ListProviders() []ProviderInfo {
r.mu.RLock()
defer r.mu.RUnlock()
infos := make([]ProviderInfo, 0, len(r.providers))
for _, name := range r.order {
if provider, exists := r.providers[name]; exists {
infos = append(infos, provider.Info())
}
}
return infos
}
// GetQuoteProviders returns all quote providers sorted by priority
func (r *Registry) GetQuoteProviders() []QuoteProvider {
r.mu.RLock()
defer r.mu.RUnlock()
providers := make([]QuoteProvider, 0)
for _, name := range r.order {
if provider, exists := r.providers[name]; exists {
if qp, ok := provider.(QuoteProvider); ok {
providers = append(providers, qp)
}
}
}
return providers
}
// GetLiquidityProviders returns all liquidity providers
func (r *Registry) GetLiquidityProviders() []LiquidityProvider {
r.mu.RLock()
defer r.mu.RUnlock()
providers := make([]LiquidityProvider, 0)
for _, name := range r.order {
if provider, exists := r.providers[name]; exists {
if lp, ok := provider.(LiquidityProvider); ok {
providers = append(providers, lp)
}
}
}
return providers
}
// GetPriceProviders returns all price providers
func (r *Registry) GetPriceProviders() []PriceProvider {
r.mu.RLock()
defer r.mu.RUnlock()
providers := make([]PriceProvider, 0)
for _, name := range r.order {
if provider, exists := r.providers[name]; exists {
if pp, ok := provider.(PriceProvider); ok {
providers = append(providers, pp)
}
}
}
return providers
}
// GetConversionProviders returns all conversion providers
func (r *Registry) GetConversionProviders() []ConversionProvider {
r.mu.RLock()
defer r.mu.RUnlock()
providers := make([]ConversionProvider, 0)
for _, name := range r.order {
if provider, exists := r.providers[name]; exists {
if cp, ok := provider.(ConversionProvider); ok {
providers = append(providers, cp)
}
}
}
return providers
}
// GetTokenListProviders returns all token list providers
func (r *Registry) GetTokenListProviders() []TokenListProvider {
r.mu.RLock()
defer r.mu.RUnlock()
providers := make([]TokenListProvider, 0)
for _, name := range r.order {
if provider, exists := r.providers[name]; exists {
if tp, ok := provider.(TokenListProvider); ok {
providers = append(providers, tp)
}
}
}
return providers
}
// sortByPriority sorts providers by priority (lower = higher priority)
func (r *Registry) sortByPriority() {
sort.Slice(r.order, func(i, j int) bool {
pi := r.providers[r.order[i]].Info().Priority
pj := r.providers[r.order[j]].Info().Priority
return pi < pj
})
}
// HealthCheckAll performs health checks on all providers
func (r *Registry) HealthCheckAll(ctx context.Context) []HealthCheck {
r.mu.RLock()
providers := make([]Provider, 0, len(r.providers))
for _, name := range r.order {
providers = append(providers, r.providers[name])
}
r.mu.RUnlock()
results := make([]HealthCheck, len(providers))
var wg sync.WaitGroup
for i, provider := range providers {
wg.Add(1)
go func(idx int, p Provider) {
defer wg.Done()
results[idx] = p.HealthCheck(ctx)
}(i, provider)
}
wg.Wait()
return results
}
// Close closes all providers
func (r *Registry) Close() error {
r.mu.Lock()
defer r.mu.Unlock()
var lastErr error
for _, provider := range r.providers {
if err := provider.Close(); err != nil {
lastErr = err
}
}
r.providers = make(map[string]Provider)
r.order = make([]string, 0)
return lastErr
}
+320
View File
@@ -0,0 +1,320 @@
package gateway
import (
"context"
"sort"
"sync"
)
// Router aggregates multiple providers and routes requests appropriately
type Router struct {
registry *Registry
enableFallback bool
}
// NewRouter creates a new router
func NewRouter(registry *Registry, enableFallback bool) *Router {
return &Router{
registry: registry,
enableFallback: enableFallback,
}
}
// GetBestQuote gets the best quote from all providers
func (r *Router) GetBestQuote(ctx context.Context, req QuoteRequest) (*SwapQuote, error) {
providers := r.registry.GetQuoteProviders()
if len(providers) == 0 {
return nil, ErrNoProvidersAvailable
}
// Get quotes from all providers in parallel
type result struct {
quote *SwapQuote
err error
}
results := make(chan result, len(providers))
var wg sync.WaitGroup
for _, provider := range providers {
wg.Add(1)
go func(p QuoteProvider) {
defer wg.Done()
quote, err := p.GetQuote(ctx, req)
results <- result{quote: quote, err: err}
}(provider)
}
go func() {
wg.Wait()
close(results)
}()
// Collect quotes
var quotes []SwapQuote
var lastErr error
for res := range results {
if res.err != nil {
lastErr = res.err
continue
}
quotes = append(quotes, *res.quote)
}
if len(quotes) == 0 {
if lastErr != nil {
return nil, lastErr
}
return nil, ErrAllProvidersFailed
}
// Sort by output amount (descending) for EXACT_INPUT
// or input amount (ascending) for EXACT_OUTPUT
sort.Slice(quotes, func(i, j int) bool {
if req.IsExactIn {
return quotes[i].TokenOut.Amount.Cmp(quotes[j].TokenOut.Amount) > 0
}
return quotes[i].TokenIn.Amount.Cmp(quotes[j].TokenIn.Amount) < 0
})
return &quotes[0], nil
}
// GetAllQuotes gets quotes from all providers
func (r *Router) GetAllQuotes(ctx context.Context, req QuoteRequest) ([]SwapQuote, error) {
providers := r.registry.GetQuoteProviders()
if len(providers) == 0 {
return nil, ErrNoProvidersAvailable
}
type result struct {
quotes []SwapQuote
err error
}
results := make(chan result, len(providers))
var wg sync.WaitGroup
for _, provider := range providers {
wg.Add(1)
go func(p QuoteProvider) {
defer wg.Done()
quotes, err := p.GetQuotes(ctx, req)
results <- result{quotes: quotes, err: err}
}(provider)
}
go func() {
wg.Wait()
close(results)
}()
var allQuotes []SwapQuote
for res := range results {
if res.err != nil {
continue
}
allQuotes = append(allQuotes, res.quotes...)
}
if len(allQuotes) == 0 {
return nil, ErrAllProvidersFailed
}
// Sort by output amount
sort.Slice(allQuotes, func(i, j int) bool {
if req.IsExactIn {
return allQuotes[i].TokenOut.Amount.Cmp(allQuotes[j].TokenOut.Amount) > 0
}
return allQuotes[i].TokenIn.Amount.Cmp(allQuotes[j].TokenIn.Amount) < 0
})
return allQuotes, nil
}
// GetPools gets pools from providers with fallback
func (r *Router) GetPools(ctx context.Context, req PoolsRequest) ([]Pool, error) {
providers := r.registry.GetLiquidityProviders()
if len(providers) == 0 {
return nil, ErrNoProvidersAvailable
}
for _, provider := range providers {
pools, err := provider.GetPools(ctx, req)
if err == nil && len(pools) > 0 {
return pools, nil
}
if !r.enableFallback {
return nil, err
}
}
return nil, ErrAllProvidersFailed
}
// GetPool gets a specific pool with fallback
func (r *Router) GetPool(ctx context.Context, chainID ChainID, address string) (*Pool, error) {
providers := r.registry.GetLiquidityProviders()
if len(providers) == 0 {
return nil, ErrNoProvidersAvailable
}
var lastErr error
for _, provider := range providers {
pool, err := provider.GetPool(ctx, chainID, address)
if err == nil {
return pool, nil
}
lastErr = err
if !r.enableFallback {
return nil, err
}
}
return nil, lastErr
}
// GetPositions gets positions with fallback
func (r *Router) GetPositions(ctx context.Context, req PositionsRequest) ([]Position, error) {
providers := r.registry.GetLiquidityProviders()
if len(providers) == 0 {
return nil, ErrNoProvidersAvailable
}
var lastErr error
for _, provider := range providers {
positions, err := provider.GetPositions(ctx, req)
if err == nil {
return positions, nil
}
lastErr = err
if !r.enableFallback {
return nil, err
}
}
return nil, lastErr
}
// GetTokenPrice gets token price from the first available provider
func (r *Router) GetTokenPrice(ctx context.Context, token Token) (*TokenPrice, error) {
providers := r.registry.GetPriceProviders()
if len(providers) == 0 {
return nil, ErrNoProvidersAvailable
}
var lastErr error
for _, provider := range providers {
price, err := provider.GetTokenPrice(ctx, token)
if err == nil {
return price, nil
}
lastErr = err
if !r.enableFallback {
return nil, err
}
}
return nil, lastErr
}
// GetTokenPrices gets token prices from all providers, aggregating results
func (r *Router) GetTokenPrices(ctx context.Context, tokens []Token) ([]TokenPrice, error) {
providers := r.registry.GetPriceProviders()
if len(providers) == 0 {
return nil, ErrNoProvidersAvailable
}
// Try first provider
for _, provider := range providers {
prices, err := provider.GetTokenPrices(ctx, tokens)
if err == nil && len(prices) > 0 {
return prices, nil
}
if !r.enableFallback {
return nil, err
}
}
return nil, ErrAllProvidersFailed
}
// CreateLead creates a conversion lead
func (r *Router) CreateLead(ctx context.Context, lead ConversionLead) (*ConversionLead, error) {
providers := r.registry.GetConversionProviders()
if len(providers) == 0 {
return nil, ErrNoProvidersAvailable
}
var lastErr error
for _, provider := range providers {
created, err := provider.CreateLead(ctx, lead)
if err == nil {
return created, nil
}
lastErr = err
if !r.enableFallback {
return nil, err
}
}
return nil, lastErr
}
// TrackEvent tracks a conversion event to all providers
func (r *Router) TrackEvent(ctx context.Context, event ConversionEvent) error {
providers := r.registry.GetConversionProviders()
if len(providers) == 0 {
return ErrNoProvidersAvailable
}
// Track to all providers (don't fail if one fails)
var lastErr error
for _, provider := range providers {
if err := provider.TrackEvent(ctx, event); err != nil {
lastErr = err
}
}
return lastErr
}
// GetTokenList gets the token list for a chain from the first available provider
func (r *Router) GetTokenList(ctx context.Context, chainID ChainID) ([]Token, error) {
providers := r.registry.GetTokenListProviders()
if len(providers) == 0 {
return nil, ErrNoProvidersAvailable
}
var lastErr error
for _, provider := range providers {
tokens, err := provider.GetTokenList(ctx, chainID)
if err == nil && len(tokens) > 0 {
return tokens, nil
}
lastErr = err
if !r.enableFallback {
return nil, err
}
}
return nil, lastErr
}
// HealthCheck performs health checks on all providers
func (r *Router) HealthCheck(ctx context.Context) []HealthCheck {
return r.registry.HealthCheckAll(ctx)
}
// ListProviders lists all registered providers
func (r *Router) ListProviders() []ProviderInfo {
return r.registry.ListProviders()
}
+497
View File
@@ -0,0 +1,497 @@
package gateway
import (
"context"
"encoding/json"
"fmt"
"math/big"
"net/http"
"strconv"
"strings"
"time"
"github.com/google/uuid"
)
// Server is the HTTP server for the gateway
type Server struct {
router *Router
httpServer *http.Server
mux *http.ServeMux
}
// ServerConfig holds server configuration
type ServerConfig struct {
Addr string
ReadTimeout time.Duration
WriteTimeout time.Duration
MaxHeaderBytes int
}
// DefaultServerConfig returns default server configuration
func DefaultServerConfig() ServerConfig {
return ServerConfig{
Addr: ":8080",
ReadTimeout: 30 * time.Second,
WriteTimeout: 60 * time.Second,
MaxHeaderBytes: 1 << 20, // 1 MB
}
}
// NewServer creates a new gateway server
func NewServer(router *Router, cfg ServerConfig) *Server {
mux := http.NewServeMux()
s := &Server{
router: router,
mux: mux,
httpServer: &http.Server{
Addr: cfg.Addr,
Handler: mux,
ReadTimeout: cfg.ReadTimeout,
WriteTimeout: cfg.WriteTimeout,
MaxHeaderBytes: cfg.MaxHeaderBytes,
},
}
s.registerRoutes()
return s
}
// Start starts the server
func (s *Server) Start() error {
return s.httpServer.ListenAndServe()
}
// Shutdown gracefully shuts down the server
func (s *Server) Shutdown(ctx context.Context) error {
return s.httpServer.Shutdown(ctx)
}
// registerRoutes registers all HTTP routes
func (s *Server) registerRoutes() {
// Health and info
s.mux.HandleFunc("/health", s.handleHealth)
s.mux.HandleFunc("/providers", s.handleProviders)
// Quote API
s.mux.HandleFunc("/v1/quote", s.handleQuote)
s.mux.HandleFunc("/v1/quotes", s.handleQuotes)
s.mux.HandleFunc("/v1/swap", s.handleSwap)
// Liquidity API
s.mux.HandleFunc("/v1/pools", s.handlePools)
s.mux.HandleFunc("/v1/pool/", s.handlePool)
s.mux.HandleFunc("/v1/positions", s.handlePositions)
// Price API
s.mux.HandleFunc("/v1/price", s.handlePrice)
s.mux.HandleFunc("/v1/prices", s.handlePrices)
// Token API
s.mux.HandleFunc("/v1/tokens", s.handleTokens)
s.mux.HandleFunc("/v1/tokens/search", s.handleTokenSearch)
// Conversion tracking API
s.mux.HandleFunc("/v1/leads", s.handleLeads)
s.mux.HandleFunc("/v1/events", s.handleEvents)
}
// Response helpers
type apiResponse struct {
Success bool `json:"success"`
Data interface{} `json:"data,omitempty"`
Error string `json:"error,omitempty"`
RequestID string `json:"requestId,omitempty"`
}
func (s *Server) writeJSON(w http.ResponseWriter, status int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(apiResponse{
Success: status < 400,
Data: data,
})
}
func (s *Server) writeError(w http.ResponseWriter, status int, err error) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(apiResponse{
Success: false,
Error: err.Error(),
})
}
func (s *Server) requestContext(r *http.Request) context.Context {
requestID := r.Header.Get("X-Request-ID")
if requestID == "" {
requestID = uuid.New().String()
}
return WithRequestID(r.Context(), requestID)
}
// Health and info handlers
func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
s.writeError(w, http.StatusMethodNotAllowed, fmt.Errorf("method not allowed"))
return
}
ctx := s.requestContext(r)
checks := s.router.HealthCheck(ctx)
healthy := true
for _, check := range checks {
if !check.Healthy {
healthy = false
break
}
}
status := http.StatusOK
if !healthy {
status = http.StatusServiceUnavailable
}
s.writeJSON(w, status, map[string]interface{}{
"status": map[bool]string{true: "healthy", false: "unhealthy"}[healthy],
"providers": checks,
})
}
func (s *Server) handleProviders(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
s.writeError(w, http.StatusMethodNotAllowed, fmt.Errorf("method not allowed"))
return
}
providers := s.router.ListProviders()
s.writeJSON(w, http.StatusOK, providers)
}
// Quote handlers
type quoteRequest struct {
TokenIn string `json:"tokenIn"`
TokenOut string `json:"tokenOut"`
ChainID uint64 `json:"chainId"`
Amount string `json:"amount"`
IsExactIn bool `json:"isExactIn"`
Slippage float64 `json:"slippage,omitempty"`
}
func (s *Server) handleQuote(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
s.writeError(w, http.StatusMethodNotAllowed, fmt.Errorf("method not allowed"))
return
}
var req quoteRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
s.writeError(w, http.StatusBadRequest, fmt.Errorf("invalid request body: %w", err))
return
}
ctx := s.requestContext(r)
quote, err := s.router.GetBestQuote(ctx, s.convertQuoteRequest(req))
if err != nil {
s.writeError(w, http.StatusInternalServerError, err)
return
}
s.writeJSON(w, http.StatusOK, quote)
}
func (s *Server) handleQuotes(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
s.writeError(w, http.StatusMethodNotAllowed, fmt.Errorf("method not allowed"))
return
}
var req quoteRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
s.writeError(w, http.StatusBadRequest, fmt.Errorf("invalid request body: %w", err))
return
}
ctx := s.requestContext(r)
quotes, err := s.router.GetAllQuotes(ctx, s.convertQuoteRequest(req))
if err != nil {
s.writeError(w, http.StatusInternalServerError, err)
return
}
s.writeJSON(w, http.StatusOK, quotes)
}
func (s *Server) handleSwap(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
s.writeError(w, http.StatusMethodNotAllowed, fmt.Errorf("method not allowed"))
return
}
// TODO: Implement swap transaction building
s.writeError(w, http.StatusNotImplemented, fmt.Errorf("swap building not yet implemented"))
}
func (s *Server) convertQuoteRequest(req quoteRequest) QuoteRequest {
amount := parseBigIntStr(req.Amount)
return QuoteRequest{
TokenIn: Token{
Address: req.TokenIn,
ChainID: ChainID(req.ChainID),
},
TokenOut: Token{
Address: req.TokenOut,
ChainID: ChainID(req.ChainID),
},
Amount: amount,
IsExactIn: req.IsExactIn,
ChainID: ChainID(req.ChainID),
Slippage: req.Slippage,
}
}
// Pool handlers
func (s *Server) handlePools(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet && r.Method != http.MethodPost {
s.writeError(w, http.StatusMethodNotAllowed, fmt.Errorf("method not allowed"))
return
}
ctx := s.requestContext(r)
var req PoolsRequest
if r.Method == http.MethodPost {
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
s.writeError(w, http.StatusBadRequest, fmt.Errorf("invalid request body: %w", err))
return
}
} else {
// Parse from query params
chainID, _ := strconv.ParseUint(r.URL.Query().Get("chainId"), 10, 64)
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
offset, _ := strconv.Atoi(r.URL.Query().Get("offset"))
req = PoolsRequest{
ChainID: ChainID(chainID),
Token0: r.URL.Query().Get("token0"),
Token1: r.URL.Query().Get("token1"),
Protocol: r.URL.Query().Get("protocol"),
Limit: limit,
Offset: offset,
}
}
pools, err := s.router.GetPools(ctx, req)
if err != nil {
s.writeError(w, http.StatusInternalServerError, err)
return
}
s.writeJSON(w, http.StatusOK, pools)
}
func (s *Server) handlePool(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
s.writeError(w, http.StatusMethodNotAllowed, fmt.Errorf("method not allowed"))
return
}
// Extract pool address from path: /v1/pool/{chainId}/{address}
path := strings.TrimPrefix(r.URL.Path, "/v1/pool/")
parts := strings.Split(path, "/")
if len(parts) != 2 {
s.writeError(w, http.StatusBadRequest, fmt.Errorf("invalid pool path"))
return
}
chainID, err := strconv.ParseUint(parts[0], 10, 64)
if err != nil {
s.writeError(w, http.StatusBadRequest, fmt.Errorf("invalid chain ID"))
return
}
ctx := s.requestContext(r)
pool, err := s.router.GetPool(ctx, ChainID(chainID), parts[1])
if err != nil {
s.writeError(w, http.StatusInternalServerError, err)
return
}
s.writeJSON(w, http.StatusOK, pool)
}
func (s *Server) handlePositions(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet && r.Method != http.MethodPost {
s.writeError(w, http.StatusMethodNotAllowed, fmt.Errorf("method not allowed"))
return
}
ctx := s.requestContext(r)
var req PositionsRequest
if r.Method == http.MethodPost {
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
s.writeError(w, http.StatusBadRequest, fmt.Errorf("invalid request body: %w", err))
return
}
} else {
chainID, _ := strconv.ParseUint(r.URL.Query().Get("chainId"), 10, 64)
req = PositionsRequest{
ChainID: ChainID(chainID),
Owner: r.URL.Query().Get("owner"),
PoolID: r.URL.Query().Get("poolId"),
}
}
positions, err := s.router.GetPositions(ctx, req)
if err != nil {
s.writeError(w, http.StatusInternalServerError, err)
return
}
s.writeJSON(w, http.StatusOK, positions)
}
// Price handlers
func (s *Server) handlePrice(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
s.writeError(w, http.StatusMethodNotAllowed, fmt.Errorf("method not allowed"))
return
}
chainID, _ := strconv.ParseUint(r.URL.Query().Get("chainId"), 10, 64)
address := r.URL.Query().Get("address")
if address == "" {
s.writeError(w, http.StatusBadRequest, fmt.Errorf("address is required"))
return
}
ctx := s.requestContext(r)
price, err := s.router.GetTokenPrice(ctx, Token{
Address: address,
ChainID: ChainID(chainID),
})
if err != nil {
s.writeError(w, http.StatusInternalServerError, err)
return
}
s.writeJSON(w, http.StatusOK, price)
}
func (s *Server) handlePrices(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
s.writeError(w, http.StatusMethodNotAllowed, fmt.Errorf("method not allowed"))
return
}
var tokens []Token
if err := json.NewDecoder(r.Body).Decode(&tokens); err != nil {
s.writeError(w, http.StatusBadRequest, fmt.Errorf("invalid request body: %w", err))
return
}
ctx := s.requestContext(r)
prices, err := s.router.GetTokenPrices(ctx, tokens)
if err != nil {
s.writeError(w, http.StatusInternalServerError, err)
return
}
s.writeJSON(w, http.StatusOK, prices)
}
// Token handlers
func (s *Server) handleTokens(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
s.writeError(w, http.StatusMethodNotAllowed, fmt.Errorf("method not allowed"))
return
}
chainID, _ := strconv.ParseUint(r.URL.Query().Get("chainId"), 10, 64)
if chainID == 0 {
s.writeError(w, http.StatusBadRequest, fmt.Errorf("chainId is required"))
return
}
ctx := s.requestContext(r)
tokens, err := s.router.GetTokenList(ctx, ChainID(chainID))
if err != nil {
s.writeError(w, http.StatusInternalServerError, err)
return
}
s.writeJSON(w, http.StatusOK, tokens)
}
func (s *Server) handleTokenSearch(w http.ResponseWriter, r *http.Request) {
// TODO: Implement token search endpoint
s.writeError(w, http.StatusNotImplemented, fmt.Errorf("token search not yet implemented"))
}
// Conversion tracking handlers
func (s *Server) handleLeads(w http.ResponseWriter, r *http.Request) {
ctx := s.requestContext(r)
switch r.Method {
case http.MethodPost:
var lead ConversionLead
if err := json.NewDecoder(r.Body).Decode(&lead); err != nil {
s.writeError(w, http.StatusBadRequest, fmt.Errorf("invalid request body: %w", err))
return
}
created, err := s.router.CreateLead(ctx, lead)
if err != nil {
s.writeError(w, http.StatusInternalServerError, err)
return
}
s.writeJSON(w, http.StatusCreated, created)
default:
s.writeError(w, http.StatusMethodNotAllowed, fmt.Errorf("method not allowed"))
}
}
func (s *Server) handleEvents(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
s.writeError(w, http.StatusMethodNotAllowed, fmt.Errorf("method not allowed"))
return
}
var event ConversionEvent
if err := json.NewDecoder(r.Body).Decode(&event); err != nil {
s.writeError(w, http.StatusBadRequest, fmt.Errorf("invalid request body: %w", err))
return
}
ctx := s.requestContext(r)
if err := s.router.TrackEvent(ctx, event); err != nil {
s.writeError(w, http.StatusInternalServerError, err)
return
}
s.writeJSON(w, http.StatusAccepted, map[string]string{"status": "accepted"})
}
// Helper to parse big.Int from string
func parseBigIntStr(s string) *big.Int {
if s == "" {
return nil
}
n := new(big.Int)
n.SetString(s, 10)
return n
}
+243
View File
@@ -0,0 +1,243 @@
// Package gateway provides an API gateway layer with pluggable providers.
// It supports delegation to external APIs (like Uniswap) and native Lux providers.
package gateway
import (
"context"
"math/big"
"time"
)
// ChainID represents a blockchain chain ID
type ChainID uint64
// Common chain IDs
const (
ChainIDEthereum ChainID = 1
ChainIDArbitrum ChainID = 42161
ChainIDOptimism ChainID = 10
ChainIDPolygon ChainID = 137
ChainIDBase ChainID = 8453
ChainIDBNB ChainID = 56
ChainIDLux ChainID = 96369
ChainIDZoo ChainID = 200200
)
// Token represents a token on a specific chain
type Token struct {
Address string `json:"address"`
ChainID ChainID `json:"chainId"`
Decimals int `json:"decimals"`
Symbol string `json:"symbol"`
Name string `json:"name"`
LogoURI string `json:"logoURI,omitempty"`
}
// TokenAmount represents a token with an amount
type TokenAmount struct {
Token Token `json:"token"`
Amount *big.Int `json:"amount"`
}
// SwapQuote represents a quote for a swap
type SwapQuote struct {
TokenIn TokenAmount `json:"tokenIn"`
TokenOut TokenAmount `json:"tokenOut"`
Route []PoolHop `json:"route"`
PriceImpact float64 `json:"priceImpact"`
GasEstimate *big.Int `json:"gasEstimate"`
QuoteID string `json:"quoteId,omitempty"`
ExpiresAt time.Time `json:"expiresAt,omitempty"`
ProviderName string `json:"providerName"`
}
// PoolHop represents a single hop in a swap route
type PoolHop struct {
PoolAddress string `json:"poolAddress"`
PoolType string `json:"poolType"` // "v2", "v3", "v4"
TokenIn Token `json:"tokenIn"`
TokenOut Token `json:"tokenOut"`
Fee int `json:"fee,omitempty"` // For V3/V4 pools
}
// SwapRequest represents a request to execute a swap
type SwapRequest struct {
TokenIn Token `json:"tokenIn"`
TokenOut Token `json:"tokenOut"`
Amount *big.Int `json:"amount"`
IsExactIn bool `json:"isExactIn"`
Slippage float64 `json:"slippage"`
Recipient string `json:"recipient"`
Deadline int64 `json:"deadline,omitempty"`
PermitSignature string `json:"permitSignature,omitempty"`
}
// SwapTransaction represents the transaction data for a swap
type SwapTransaction struct {
To string `json:"to"`
Data string `json:"data"`
Value *big.Int `json:"value"`
GasLimit *big.Int `json:"gasLimit"`
ChainID ChainID `json:"chainId"`
}
// Pool represents a liquidity pool
type Pool struct {
Address string `json:"address"`
ChainID ChainID `json:"chainId"`
Protocol string `json:"protocol"` // "uniswap_v2", "uniswap_v3", "lux_amm"
Token0 Token `json:"token0"`
Token1 Token `json:"token1"`
Fee int `json:"fee,omitempty"`
TVL *big.Int `json:"tvl,omitempty"`
Volume24h *big.Int `json:"volume24h,omitempty"`
APR float64 `json:"apr,omitempty"`
TickData []TickRange `json:"tickData,omitempty"` // For V3/V4 pools
Reserves *Reserves `json:"reserves,omitempty"` // For V2 pools
}
// Reserves represents V2 pool reserves
type Reserves struct {
Reserve0 *big.Int `json:"reserve0"`
Reserve1 *big.Int `json:"reserve1"`
}
// TickRange represents a tick range with liquidity (V3/V4)
type TickRange struct {
TickLower int `json:"tickLower"`
TickUpper int `json:"tickUpper"`
Liquidity *big.Int `json:"liquidity"`
}
// Position represents a liquidity position
type Position struct {
ID string `json:"id"`
Owner string `json:"owner"`
Pool Pool `json:"pool"`
Liquidity *big.Int `json:"liquidity"`
Token0Owed *big.Int `json:"token0Owed"`
Token1Owed *big.Int `json:"token1Owed"`
TickLower int `json:"tickLower,omitempty"`
TickUpper int `json:"tickUpper,omitempty"`
FeesEarned *FeesEarned `json:"feesEarned,omitempty"`
}
// FeesEarned represents fees earned from a position
type FeesEarned struct {
Token0Fees *big.Int `json:"token0Fees"`
Token1Fees *big.Int `json:"token1Fees"`
}
// ConversionLead represents a conversion tracking lead
type ConversionLead struct {
ID string `json:"id"`
Source string `json:"source"`
Medium string `json:"medium,omitempty"`
Campaign string `json:"campaign,omitempty"`
WalletAddr string `json:"walletAddr,omitempty"`
Timestamp time.Time `json:"timestamp"`
Metadata map[string]string `json:"metadata,omitempty"`
}
// ConversionEvent represents a conversion event
type ConversionEvent struct {
LeadID string `json:"leadId"`
EventType string `json:"eventType"` // "swap", "lp_add", "wallet_connect"
ChainID ChainID `json:"chainId"`
TxHash string `json:"txHash,omitempty"`
Value *big.Int `json:"value,omitempty"`
Timestamp time.Time `json:"timestamp"`
Metadata map[string]string `json:"metadata,omitempty"`
}
// TokenPrice represents token price data
type TokenPrice struct {
Token Token `json:"token"`
PriceUSD float64 `json:"priceUSD"`
PriceChange float64 `json:"priceChange24h,omitempty"`
Volume24h *big.Int `json:"volume24h,omitempty"`
MarketCap *big.Int `json:"marketCap,omitempty"`
UpdatedAt time.Time `json:"updatedAt"`
}
// ProviderInfo contains information about a provider
type ProviderInfo struct {
Name string `json:"name"`
Version string `json:"version"`
Description string `json:"description"`
SupportedChains []ChainID `json:"supportedChains"`
Priority int `json:"priority"` // Lower = higher priority
Healthy bool `json:"healthy"`
}
// HealthCheck represents health check result
type HealthCheck struct {
Provider string `json:"provider"`
Healthy bool `json:"healthy"`
Latency int64 `json:"latencyMs"`
LastCheck time.Time `json:"lastCheck"`
Error string `json:"error,omitempty"`
}
// QuoteRequest for getting swap quotes
type QuoteRequest struct {
TokenIn Token `json:"tokenIn"`
TokenOut Token `json:"tokenOut"`
Amount *big.Int `json:"amount"`
IsExactIn bool `json:"isExactIn"`
ChainID ChainID `json:"chainId"`
Slippage float64 `json:"slippage,omitempty"`
}
// PoolsRequest for querying pools
type PoolsRequest struct {
ChainID ChainID `json:"chainId"`
Token0 string `json:"token0,omitempty"`
Token1 string `json:"token1,omitempty"`
Protocol string `json:"protocol,omitempty"`
MinTVL *big.Int `json:"minTvl,omitempty"`
Limit int `json:"limit,omitempty"`
Offset int `json:"offset,omitempty"`
}
// PositionsRequest for querying positions
type PositionsRequest struct {
ChainID ChainID `json:"chainId"`
Owner string `json:"owner"`
PoolID string `json:"poolId,omitempty"`
}
// Error types
type ProviderError struct {
Provider string
Err error
}
func (e *ProviderError) Error() string {
return e.Provider + ": " + e.Err.Error()
}
func (e *ProviderError) Unwrap() error {
return e.Err
}
// Context key type
type contextKey string
const (
ContextKeyRequestID contextKey = "requestID"
ContextKeyChainID contextKey = "chainID"
)
// WithRequestID adds a request ID to the context
func WithRequestID(ctx context.Context, requestID string) context.Context {
return context.WithValue(ctx, ContextKeyRequestID, requestID)
}
// GetRequestID gets the request ID from context
func GetRequestID(ctx context.Context) string {
if v := ctx.Value(ContextKeyRequestID); v != nil {
return v.(string)
}
return ""
}
+403
View File
@@ -0,0 +1,403 @@
// Package uniswap provides a Uniswap API provider implementation.
// This delegates to Uniswap's backend APIs as a fallback provider
// until native Lux infrastructure is deployed.
package uniswap
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
)
// APIEndpoints contains the Uniswap API endpoints
type APIEndpoints struct {
// Core API for quotes, tokens, and general data
CoreAPI string
// Liquidity backend for pool and position data
LiquidityAPI string
// Conversion tracking/entry gateway
ConversionAPI string
}
// DefaultEndpoints returns the default Uniswap production endpoints
func DefaultEndpoints() APIEndpoints {
return APIEndpoints{
CoreAPI: "https://api.uniswap.org",
LiquidityAPI: "https://liquidity.backend-prod.api.uniswap.org",
ConversionAPI: "https://entry-gateway.backend-prod.api.uniswap.org",
}
}
// Client is an HTTP client for Uniswap APIs
type Client struct {
endpoints APIEndpoints
httpClient *http.Client
apiKey string
userAgent string
}
// ClientConfig holds client configuration
type ClientConfig struct {
Endpoints APIEndpoints
APIKey string
Timeout time.Duration
UserAgent string
}
// NewClient creates a new Uniswap API client
func NewClient(cfg ClientConfig) *Client {
if cfg.Endpoints.CoreAPI == "" {
cfg.Endpoints = DefaultEndpoints()
}
if cfg.Timeout == 0 {
cfg.Timeout = 30 * time.Second
}
if cfg.UserAgent == "" {
cfg.UserAgent = "Lux-DEX-Gateway/1.0"
}
return &Client{
endpoints: cfg.Endpoints,
httpClient: &http.Client{
Timeout: cfg.Timeout,
},
apiKey: cfg.APIKey,
userAgent: cfg.UserAgent,
}
}
// request makes an HTTP request to a Uniswap API
func (c *Client) request(ctx context.Context, method, url string, body interface{}, result interface{}) error {
var bodyReader io.Reader
if body != nil {
data, err := json.Marshal(body)
if err != nil {
return fmt.Errorf("failed to marshal request body: %w", err)
}
bodyReader = bytes.NewReader(data)
}
req, err := http.NewRequestWithContext(ctx, method, url, bodyReader)
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("User-Agent", c.userAgent)
if c.apiKey != "" {
req.Header.Set("X-API-Key", c.apiKey)
}
resp, err := c.httpClient.Do(req)
if err != nil {
return fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("failed to read response: %w", err)
}
if resp.StatusCode >= 400 {
return &APIError{
StatusCode: resp.StatusCode,
Message: string(respBody),
}
}
if result != nil {
if err := json.Unmarshal(respBody, result); err != nil {
return fmt.Errorf("failed to unmarshal response: %w", err)
}
}
return nil
}
// APIError represents an API error response
type APIError struct {
StatusCode int
Message string
}
func (e *APIError) Error() string {
return fmt.Sprintf("uniswap API error (status %d): %s", e.StatusCode, e.Message)
}
// HealthCheck performs a health check on the APIs
func (c *Client) HealthCheck(ctx context.Context) error {
// Simple health check - just verify the core API is reachable
req, err := http.NewRequestWithContext(ctx, "GET", c.endpoints.CoreAPI+"/health", nil)
if err != nil {
return err
}
req.Header.Set("User-Agent", c.userAgent)
resp, err := c.httpClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
return fmt.Errorf("health check failed with status %d", resp.StatusCode)
}
return nil
}
// Core API methods
// QuoteRequest for the Uniswap API
type QuoteAPIRequest struct {
TokenIn string `json:"tokenIn"`
TokenInChainID uint64 `json:"tokenInChainId"`
TokenOut string `json:"tokenOut"`
TokenOutChainID uint64 `json:"tokenOutChainId"`
Amount string `json:"amount"`
Type string `json:"type"` // "EXACT_INPUT" or "EXACT_OUTPUT"
Slippage int `json:"slippageTolerance,omitempty"` // in bps
Protocols string `json:"protocols,omitempty"`
Recipient string `json:"recipient,omitempty"`
}
// QuoteAPIResponse from the Uniswap API
type QuoteAPIResponse struct {
Quote string `json:"quote"`
QuoteGasAdjusted string `json:"quoteGasAdjusted"`
GasUseEstimate string `json:"gasUseEstimate"`
GasUseEstimateQuote string `json:"gasUseEstimateQuote"`
Route []RouteAPIData `json:"route"`
PriceImpact string `json:"priceImpact,omitempty"`
MethodParameters *MethodParams `json:"methodParameters,omitempty"`
}
// RouteAPIData represents a route from the API
type RouteAPIData struct {
Type string `json:"type"`
Address string `json:"address"`
TokenIn TokenAPIData `json:"tokenIn"`
TokenOut TokenAPIData `json:"tokenOut"`
Fee string `json:"fee,omitempty"`
AmountIn string `json:"amountIn,omitempty"`
AmountOut string `json:"amountOut,omitempty"`
}
// TokenAPIData represents token data from the API
type TokenAPIData struct {
ChainID uint64 `json:"chainId"`
Decimals int `json:"decimals"`
Address string `json:"address"`
Symbol string `json:"symbol"`
Name string `json:"name,omitempty"`
}
// MethodParams contains transaction method parameters
type MethodParams struct {
Calldata string `json:"calldata"`
Value string `json:"value"`
To string `json:"to"`
}
// GetQuote gets a swap quote from the Core API
func (c *Client) GetQuote(ctx context.Context, req QuoteAPIRequest) (*QuoteAPIResponse, error) {
url := fmt.Sprintf("%s/v2/quote", c.endpoints.CoreAPI)
var resp QuoteAPIResponse
if err := c.request(ctx, "POST", url, req, &resp); err != nil {
return nil, err
}
return &resp, nil
}
// Liquidity API methods
// PoolsAPIRequest for querying pools
type PoolsAPIRequest struct {
ChainID uint64 `json:"chainId"`
TokenA string `json:"tokenA,omitempty"`
TokenB string `json:"tokenB,omitempty"`
PoolType string `json:"poolType,omitempty"`
MinTVL float64 `json:"minTvl,omitempty"`
Limit int `json:"limit,omitempty"`
Offset int `json:"offset,omitempty"`
}
// PoolAPIData represents pool data from the API
type PoolAPIData struct {
ID string `json:"id"`
Address string `json:"address"`
ChainID uint64 `json:"chainId"`
Protocol string `json:"protocol"`
Token0 TokenAPIData `json:"token0"`
Token1 TokenAPIData `json:"token1"`
FeeTier int `json:"feeTier,omitempty"`
TVL string `json:"tvl"`
Volume24h string `json:"volume24h"`
APR float64 `json:"apr,omitempty"`
}
// GetPools queries pools from the Liquidity API
func (c *Client) GetPools(ctx context.Context, req PoolsAPIRequest) ([]PoolAPIData, error) {
url := fmt.Sprintf("%s/v1/pools", c.endpoints.LiquidityAPI)
var resp struct {
Pools []PoolAPIData `json:"pools"`
}
if err := c.request(ctx, "POST", url, req, &resp); err != nil {
return nil, err
}
return resp.Pools, nil
}
// PositionsAPIRequest for querying positions
type PositionsAPIRequest struct {
ChainID uint64 `json:"chainId"`
Owner string `json:"owner"`
PoolID string `json:"poolId,omitempty"`
}
// PositionAPIData represents position data from the API
type PositionAPIData struct {
ID string `json:"id"`
Owner string `json:"owner"`
Pool PoolAPIData `json:"pool"`
Liquidity string `json:"liquidity"`
Token0Owed string `json:"token0Owed"`
Token1Owed string `json:"token1Owed"`
TickLower int `json:"tickLower,omitempty"`
TickUpper int `json:"tickUpper,omitempty"`
Fees0 string `json:"fees0,omitempty"`
Fees1 string `json:"fees1,omitempty"`
}
// GetPositions queries positions from the Liquidity API
func (c *Client) GetPositions(ctx context.Context, req PositionsAPIRequest) ([]PositionAPIData, error) {
url := fmt.Sprintf("%s/v1/positions", c.endpoints.LiquidityAPI)
var resp struct {
Positions []PositionAPIData `json:"positions"`
}
if err := c.request(ctx, "POST", url, req, &resp); err != nil {
return nil, err
}
return resp.Positions, nil
}
// Conversion API methods
// LeadAPIData represents a conversion lead
type LeadAPIData struct {
ID string `json:"id"`
Source string `json:"source"`
Medium string `json:"medium,omitempty"`
Campaign string `json:"campaign,omitempty"`
WalletAddr string `json:"walletAddr,omitempty"`
CreatedAt string `json:"createdAt"`
Metadata map[string]string `json:"metadata,omitempty"`
}
// CreateLead creates a conversion lead
func (c *Client) CreateLead(ctx context.Context, lead LeadAPIData) (*LeadAPIData, error) {
url := fmt.Sprintf("%s/v1/leads", c.endpoints.ConversionAPI)
var resp LeadAPIData
if err := c.request(ctx, "POST", url, lead, &resp); err != nil {
return nil, err
}
return &resp, nil
}
// EventAPIData represents a conversion event
type EventAPIData struct {
LeadID string `json:"leadId"`
EventType string `json:"eventType"`
ChainID uint64 `json:"chainId"`
TxHash string `json:"txHash,omitempty"`
Value string `json:"value,omitempty"`
Timestamp string `json:"timestamp"`
Metadata map[string]string `json:"metadata,omitempty"`
}
// TrackEvent tracks a conversion event
func (c *Client) TrackEvent(ctx context.Context, event EventAPIData) error {
url := fmt.Sprintf("%s/v1/events", c.endpoints.ConversionAPI)
return c.request(ctx, "POST", url, event, nil)
}
// GetLeadEvents gets events for a lead
func (c *Client) GetLeadEvents(ctx context.Context, leadID string) ([]EventAPIData, error) {
url := fmt.Sprintf("%s/v1/leads/%s/events", c.endpoints.ConversionAPI, leadID)
var resp struct {
Events []EventAPIData `json:"events"`
}
if err := c.request(ctx, "GET", url, nil, &resp); err != nil {
return nil, err
}
return resp.Events, nil
}
// Token list methods
// GetTokenList gets the token list for a chain
func (c *Client) GetTokenList(ctx context.Context, chainID uint64) ([]TokenAPIData, error) {
url := fmt.Sprintf("%s/v1/tokens?chainId=%d", c.endpoints.CoreAPI, chainID)
var resp struct {
Tokens []TokenAPIData `json:"tokens"`
}
if err := c.request(ctx, "GET", url, nil, &resp); err != nil {
return nil, err
}
return resp.Tokens, nil
}
// SearchTokens searches for tokens
func (c *Client) SearchTokens(ctx context.Context, chainID uint64, query string) ([]TokenAPIData, error) {
url := fmt.Sprintf("%s/v1/tokens/search?chainId=%d&query=%s", c.endpoints.CoreAPI, chainID, query)
var resp struct {
Tokens []TokenAPIData `json:"tokens"`
}
if err := c.request(ctx, "GET", url, nil, &resp); err != nil {
return nil, err
}
return resp.Tokens, nil
}
// GetTokenPrice gets token price
func (c *Client) GetTokenPrice(ctx context.Context, chainID uint64, address string) (*TokenPriceAPIData, error) {
url := fmt.Sprintf("%s/v1/price?chainId=%d&address=%s", c.endpoints.CoreAPI, chainID, address)
var resp TokenPriceAPIData
if err := c.request(ctx, "GET", url, nil, &resp); err != nil {
return nil, err
}
return &resp, nil
}
// TokenPriceAPIData represents token price from API
type TokenPriceAPIData struct {
ChainID uint64 `json:"chainId"`
Address string `json:"address"`
PriceUSD float64 `json:"priceUSD"`
PriceChange24h float64 `json:"priceChange24h,omitempty"`
Volume24h string `json:"volume24h,omitempty"`
MarketCap string `json:"marketCap,omitempty"`
}
+23
View File
@@ -0,0 +1,23 @@
package uniswap
import "errors"
var (
// ErrNoMethodParameters indicates quote response missing method parameters
ErrNoMethodParameters = errors.New("quote response missing method parameters")
// ErrExecuteNotSupported indicates server-side swap execution is not supported
ErrExecuteNotSupported = errors.New("server-side swap execution not supported")
// ErrPoolNotFound indicates the requested pool was not found
ErrPoolNotFound = errors.New("pool not found")
// ErrPositionNotFound indicates the requested position was not found
ErrPositionNotFound = errors.New("position not found")
// ErrTokenNotFound indicates the requested token was not found
ErrTokenNotFound = errors.New("token not found")
// ErrNotImplemented indicates the feature is not implemented
ErrNotImplemented = errors.New("feature not implemented in Uniswap provider")
)
+572
View File
@@ -0,0 +1,572 @@
package uniswap
import (
"context"
"math/big"
"strconv"
"time"
"github.com/luxfi/dex/pkg/gateway"
)
// Provider implements the gateway provider interfaces using Uniswap APIs
type Provider struct {
client *Client
name string
priority int
chains []gateway.ChainID
}
// ProviderConfig holds provider configuration
type ProviderConfig struct {
Name string
Priority int
APIKey string
Timeout time.Duration
Endpoints APIEndpoints
Chains []gateway.ChainID
}
// DefaultConfig returns the default provider configuration
func DefaultConfig() ProviderConfig {
return ProviderConfig{
Name: "uniswap",
Priority: 100, // Default priority (lower = higher priority)
Timeout: 30 * time.Second,
Chains: []gateway.ChainID{
gateway.ChainIDEthereum,
gateway.ChainIDArbitrum,
gateway.ChainIDOptimism,
gateway.ChainIDPolygon,
gateway.ChainIDBase,
gateway.ChainIDBNB,
},
}
}
// NewProvider creates a new Uniswap provider
func NewProvider(cfg ProviderConfig) *Provider {
if cfg.Name == "" {
cfg.Name = "uniswap"
}
if cfg.Priority == 0 {
cfg.Priority = 100
}
if len(cfg.Chains) == 0 {
cfg.Chains = DefaultConfig().Chains
}
client := NewClient(ClientConfig{
Endpoints: cfg.Endpoints,
APIKey: cfg.APIKey,
Timeout: cfg.Timeout,
})
return &Provider{
client: client,
name: cfg.Name,
priority: cfg.Priority,
chains: cfg.Chains,
}
}
// Info returns provider information
func (p *Provider) Info() gateway.ProviderInfo {
return gateway.ProviderInfo{
Name: p.name,
Version: "1.0.0",
Description: "Uniswap API provider for quotes, liquidity, and conversion tracking",
SupportedChains: p.chains,
Priority: p.priority,
Healthy: true, // Will be updated by health checks
}
}
// HealthCheck performs a health check
func (p *Provider) HealthCheck(ctx context.Context) gateway.HealthCheck {
start := time.Now()
err := p.client.HealthCheck(ctx)
latency := time.Since(start).Milliseconds()
result := gateway.HealthCheck{
Provider: p.name,
Healthy: err == nil,
Latency: latency,
LastCheck: time.Now(),
}
if err != nil {
result.Error = err.Error()
}
return result
}
// Close cleans up resources
func (p *Provider) Close() error {
return nil
}
// GetQuote returns a swap quote
func (p *Provider) GetQuote(ctx context.Context, req gateway.QuoteRequest) (*gateway.SwapQuote, error) {
quoteType := "EXACT_INPUT"
if !req.IsExactIn {
quoteType = "EXACT_OUTPUT"
}
apiReq := QuoteAPIRequest{
TokenIn: req.TokenIn.Address,
TokenInChainID: uint64(req.TokenIn.ChainID),
TokenOut: req.TokenOut.Address,
TokenOutChainID: uint64(req.TokenOut.ChainID),
Amount: req.Amount.String(),
Type: quoteType,
}
if req.Slippage > 0 {
apiReq.Slippage = int(req.Slippage * 10000) // Convert to bps
}
apiResp, err := p.client.GetQuote(ctx, apiReq)
if err != nil {
return nil, &gateway.ProviderError{Provider: p.name, Err: err}
}
return p.convertQuoteResponse(req, apiResp), nil
}
// GetQuotes returns multiple quotes
func (p *Provider) GetQuotes(ctx context.Context, req gateway.QuoteRequest) ([]gateway.SwapQuote, error) {
quote, err := p.GetQuote(ctx, req)
if err != nil {
return nil, err
}
return []gateway.SwapQuote{*quote}, nil
}
// BuildSwap builds a swap transaction
func (p *Provider) BuildSwap(ctx context.Context, quote gateway.SwapQuote, recipient string, deadline int64) (*gateway.SwapTransaction, error) {
quoteType := "EXACT_INPUT"
apiReq := QuoteAPIRequest{
TokenIn: quote.TokenIn.Token.Address,
TokenInChainID: uint64(quote.TokenIn.Token.ChainID),
TokenOut: quote.TokenOut.Token.Address,
TokenOutChainID: uint64(quote.TokenOut.Token.ChainID),
Amount: quote.TokenIn.Amount.String(),
Type: quoteType,
Recipient: recipient,
}
apiResp, err := p.client.GetQuote(ctx, apiReq)
if err != nil {
return nil, &gateway.ProviderError{Provider: p.name, Err: err}
}
if apiResp.MethodParameters == nil {
return nil, &gateway.ProviderError{
Provider: p.name,
Err: ErrNoMethodParameters,
}
}
value := new(big.Int)
if apiResp.MethodParameters.Value != "" {
value.SetString(apiResp.MethodParameters.Value, 10)
}
gasLimit := new(big.Int)
if apiResp.GasUseEstimate != "" {
gasLimit.SetString(apiResp.GasUseEstimate, 10)
// Add 20% buffer
gasLimit.Mul(gasLimit, big.NewInt(120))
gasLimit.Div(gasLimit, big.NewInt(100))
}
return &gateway.SwapTransaction{
To: apiResp.MethodParameters.To,
Data: apiResp.MethodParameters.Calldata,
Value: value,
GasLimit: gasLimit,
ChainID: quote.TokenIn.Token.ChainID,
}, nil
}
// ExecuteSwap executes a swap (not supported - returns tx data only)
func (p *Provider) ExecuteSwap(ctx context.Context, req gateway.SwapRequest) (string, error) {
return "", &gateway.ProviderError{
Provider: p.name,
Err: ErrExecuteNotSupported,
}
}
// GetPools returns pools matching criteria
func (p *Provider) GetPools(ctx context.Context, req gateway.PoolsRequest) ([]gateway.Pool, error) {
apiReq := PoolsAPIRequest{
ChainID: uint64(req.ChainID),
TokenA: req.Token0,
TokenB: req.Token1,
Limit: req.Limit,
Offset: req.Offset,
}
if req.MinTVL != nil {
tvl, _ := req.MinTVL.Float64()
apiReq.MinTVL = tvl
}
apiPools, err := p.client.GetPools(ctx, apiReq)
if err != nil {
return nil, &gateway.ProviderError{Provider: p.name, Err: err}
}
pools := make([]gateway.Pool, len(apiPools))
for i, ap := range apiPools {
pools[i] = p.convertPoolData(ap)
}
return pools, nil
}
// GetPool returns a specific pool
func (p *Provider) GetPool(ctx context.Context, chainID gateway.ChainID, address string) (*gateway.Pool, error) {
pools, err := p.GetPools(ctx, gateway.PoolsRequest{
ChainID: chainID,
Token0: address,
Limit: 1,
})
if err != nil {
return nil, err
}
if len(pools) == 0 {
return nil, &gateway.ProviderError{Provider: p.name, Err: ErrPoolNotFound}
}
return &pools[0], nil
}
// GetPositions returns positions for an owner
func (p *Provider) GetPositions(ctx context.Context, req gateway.PositionsRequest) ([]gateway.Position, error) {
apiReq := PositionsAPIRequest{
ChainID: uint64(req.ChainID),
Owner: req.Owner,
PoolID: req.PoolID,
}
apiPositions, err := p.client.GetPositions(ctx, apiReq)
if err != nil {
return nil, &gateway.ProviderError{Provider: p.name, Err: err}
}
positions := make([]gateway.Position, len(apiPositions))
for i, ap := range apiPositions {
positions[i] = p.convertPositionData(ap)
}
return positions, nil
}
// GetPosition returns a specific position
func (p *Provider) GetPosition(ctx context.Context, chainID gateway.ChainID, positionID string) (*gateway.Position, error) {
// Query all positions and find the matching one
positions, err := p.GetPositions(ctx, gateway.PositionsRequest{
ChainID: chainID,
})
if err != nil {
return nil, err
}
for _, pos := range positions {
if pos.ID == positionID {
return &pos, nil
}
}
return nil, &gateway.ProviderError{Provider: p.name, Err: ErrPositionNotFound}
}
// BuildAddLiquidity builds add liquidity transaction (not implemented)
func (p *Provider) BuildAddLiquidity(ctx context.Context, pool gateway.Pool, amount0, amount1 string, tickLower, tickUpper int) (*gateway.SwapTransaction, error) {
return nil, &gateway.ProviderError{Provider: p.name, Err: ErrNotImplemented}
}
// BuildRemoveLiquidity builds remove liquidity transaction (not implemented)
func (p *Provider) BuildRemoveLiquidity(ctx context.Context, position gateway.Position, percentage float64) (*gateway.SwapTransaction, error) {
return nil, &gateway.ProviderError{Provider: p.name, Err: ErrNotImplemented}
}
// BuildCollectFees builds collect fees transaction (not implemented)
func (p *Provider) BuildCollectFees(ctx context.Context, position gateway.Position) (*gateway.SwapTransaction, error) {
return nil, &gateway.ProviderError{Provider: p.name, Err: ErrNotImplemented}
}
// GetTokenPrice returns token price
func (p *Provider) GetTokenPrice(ctx context.Context, token gateway.Token) (*gateway.TokenPrice, error) {
apiPrice, err := p.client.GetTokenPrice(ctx, uint64(token.ChainID), token.Address)
if err != nil {
return nil, &gateway.ProviderError{Provider: p.name, Err: err}
}
return &gateway.TokenPrice{
Token: token,
PriceUSD: apiPrice.PriceUSD,
PriceChange: apiPrice.PriceChange24h,
Volume24h: parseBigInt(apiPrice.Volume24h),
MarketCap: parseBigInt(apiPrice.MarketCap),
UpdatedAt: time.Now(),
}, nil
}
// GetTokenPrices returns prices for multiple tokens
func (p *Provider) GetTokenPrices(ctx context.Context, tokens []gateway.Token) ([]gateway.TokenPrice, error) {
prices := make([]gateway.TokenPrice, 0, len(tokens))
for _, token := range tokens {
price, err := p.GetTokenPrice(ctx, token)
if err != nil {
continue // Skip failed price lookups
}
prices = append(prices, *price)
}
return prices, nil
}
// CreateLead creates a conversion lead
func (p *Provider) CreateLead(ctx context.Context, lead gateway.ConversionLead) (*gateway.ConversionLead, error) {
apiLead := LeadAPIData{
Source: lead.Source,
Medium: lead.Medium,
Campaign: lead.Campaign,
WalletAddr: lead.WalletAddr,
Metadata: lead.Metadata,
}
created, err := p.client.CreateLead(ctx, apiLead)
if err != nil {
return nil, &gateway.ProviderError{Provider: p.name, Err: err}
}
return p.convertLeadData(*created), nil
}
// GetLead retrieves a lead
func (p *Provider) GetLead(ctx context.Context, leadID string) (*gateway.ConversionLead, error) {
// Not directly supported - would need lead storage
return nil, &gateway.ProviderError{Provider: p.name, Err: ErrNotImplemented}
}
// TrackEvent tracks a conversion event
func (p *Provider) TrackEvent(ctx context.Context, event gateway.ConversionEvent) error {
apiEvent := EventAPIData{
LeadID: event.LeadID,
EventType: event.EventType,
ChainID: uint64(event.ChainID),
TxHash: event.TxHash,
Timestamp: event.Timestamp.Format(time.RFC3339),
Metadata: event.Metadata,
}
if event.Value != nil {
apiEvent.Value = event.Value.String()
}
if err := p.client.TrackEvent(ctx, apiEvent); err != nil {
return &gateway.ProviderError{Provider: p.name, Err: err}
}
return nil
}
// GetLeadEvents gets events for a lead
func (p *Provider) GetLeadEvents(ctx context.Context, leadID string) ([]gateway.ConversionEvent, error) {
apiEvents, err := p.client.GetLeadEvents(ctx, leadID)
if err != nil {
return nil, &gateway.ProviderError{Provider: p.name, Err: err}
}
events := make([]gateway.ConversionEvent, len(apiEvents))
for i, ae := range apiEvents {
events[i] = p.convertEventData(ae)
}
return events, nil
}
// GetTokenList returns token list for a chain
func (p *Provider) GetTokenList(ctx context.Context, chainID gateway.ChainID) ([]gateway.Token, error) {
apiTokens, err := p.client.GetTokenList(ctx, uint64(chainID))
if err != nil {
return nil, &gateway.ProviderError{Provider: p.name, Err: err}
}
tokens := make([]gateway.Token, len(apiTokens))
for i, at := range apiTokens {
tokens[i] = p.convertTokenData(at)
}
return tokens, nil
}
// SearchTokens searches for tokens
func (p *Provider) SearchTokens(ctx context.Context, chainID gateway.ChainID, query string) ([]gateway.Token, error) {
apiTokens, err := p.client.SearchTokens(ctx, uint64(chainID), query)
if err != nil {
return nil, &gateway.ProviderError{Provider: p.name, Err: err}
}
tokens := make([]gateway.Token, len(apiTokens))
for i, at := range apiTokens {
tokens[i] = p.convertTokenData(at)
}
return tokens, nil
}
// GetToken returns a specific token
func (p *Provider) GetToken(ctx context.Context, chainID gateway.ChainID, address string) (*gateway.Token, error) {
tokens, err := p.SearchTokens(ctx, chainID, address)
if err != nil {
return nil, err
}
for _, token := range tokens {
if token.Address == address {
return &token, nil
}
}
return nil, &gateway.ProviderError{Provider: p.name, Err: ErrTokenNotFound}
}
// Helper conversion functions
func (p *Provider) convertQuoteResponse(req gateway.QuoteRequest, resp *QuoteAPIResponse) *gateway.SwapQuote {
amountOut, _ := new(big.Int).SetString(resp.Quote, 10)
gasEstimate, _ := new(big.Int).SetString(resp.GasUseEstimate, 10)
route := make([]gateway.PoolHop, len(resp.Route))
for i, r := range resp.Route {
fee := 0
if r.Fee != "" {
fee, _ = strconv.Atoi(r.Fee)
}
route[i] = gateway.PoolHop{
PoolAddress: r.Address,
PoolType: r.Type,
TokenIn: p.convertTokenData(r.TokenIn),
TokenOut: p.convertTokenData(r.TokenOut),
Fee: fee,
}
}
priceImpact := 0.0
if resp.PriceImpact != "" {
priceImpact, _ = strconv.ParseFloat(resp.PriceImpact, 64)
}
return &gateway.SwapQuote{
TokenIn: gateway.TokenAmount{
Token: req.TokenIn,
Amount: req.Amount,
},
TokenOut: gateway.TokenAmount{
Token: req.TokenOut,
Amount: amountOut,
},
Route: route,
PriceImpact: priceImpact,
GasEstimate: gasEstimate,
ProviderName: p.name,
}
}
func (p *Provider) convertPoolData(ap PoolAPIData) gateway.Pool {
tvl, _ := new(big.Int).SetString(ap.TVL, 10)
volume, _ := new(big.Int).SetString(ap.Volume24h, 10)
return gateway.Pool{
Address: ap.Address,
ChainID: gateway.ChainID(ap.ChainID),
Protocol: ap.Protocol,
Token0: p.convertTokenData(ap.Token0),
Token1: p.convertTokenData(ap.Token1),
Fee: ap.FeeTier,
TVL: tvl,
Volume24h: volume,
APR: ap.APR,
}
}
func (p *Provider) convertPositionData(ap PositionAPIData) gateway.Position {
liquidity, _ := new(big.Int).SetString(ap.Liquidity, 10)
token0Owed, _ := new(big.Int).SetString(ap.Token0Owed, 10)
token1Owed, _ := new(big.Int).SetString(ap.Token1Owed, 10)
var feesEarned *gateway.FeesEarned
if ap.Fees0 != "" || ap.Fees1 != "" {
fees0, _ := new(big.Int).SetString(ap.Fees0, 10)
fees1, _ := new(big.Int).SetString(ap.Fees1, 10)
feesEarned = &gateway.FeesEarned{
Token0Fees: fees0,
Token1Fees: fees1,
}
}
return gateway.Position{
ID: ap.ID,
Owner: ap.Owner,
Pool: p.convertPoolData(ap.Pool),
Liquidity: liquidity,
Token0Owed: token0Owed,
Token1Owed: token1Owed,
TickLower: ap.TickLower,
TickUpper: ap.TickUpper,
FeesEarned: feesEarned,
}
}
func (p *Provider) convertTokenData(at TokenAPIData) gateway.Token {
return gateway.Token{
Address: at.Address,
ChainID: gateway.ChainID(at.ChainID),
Decimals: at.Decimals,
Symbol: at.Symbol,
Name: at.Name,
}
}
func (p *Provider) convertLeadData(al LeadAPIData) *gateway.ConversionLead {
ts, _ := time.Parse(time.RFC3339, al.CreatedAt)
return &gateway.ConversionLead{
ID: al.ID,
Source: al.Source,
Medium: al.Medium,
Campaign: al.Campaign,
WalletAddr: al.WalletAddr,
Timestamp: ts,
Metadata: al.Metadata,
}
}
func (p *Provider) convertEventData(ae EventAPIData) gateway.ConversionEvent {
ts, _ := time.Parse(time.RFC3339, ae.Timestamp)
value, _ := new(big.Int).SetString(ae.Value, 10)
return gateway.ConversionEvent{
LeadID: ae.LeadID,
EventType: ae.EventType,
ChainID: gateway.ChainID(ae.ChainID),
TxHash: ae.TxHash,
Value: value,
Timestamp: ts,
Metadata: ae.Metadata,
}
}
func parseBigInt(s string) *big.Int {
if s == "" {
return nil
}
n, _ := new(big.Int).SetString(s, 10)
return n
}