mirror of
https://github.com/luxfi/dex.git
synced 2026-07-27 02:57:58 +00:00
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:
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
Generated
+1236
-736
File diff suppressed because it is too large
Load Diff
Executable
BIN
Binary file not shown.
@@ -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
|
||||
)
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 "es[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()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 ""
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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")
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user