Add full monorepo: virtual-banker, backend, frontend, docs, scripts, deployment
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
101
backend/bridge/ccip_provider.go
Normal file
101
backend/bridge/ccip_provider.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
ccipTimeout = 5 * time.Second
|
||||
defaultCCIPFee = "100000000000000000" // ~0.1 LINK (18 decimals)
|
||||
)
|
||||
|
||||
// CCIP-supported chain pair: 138 <-> 1
|
||||
var ccipSupportedPairs = map[string]bool{
|
||||
"138-1": true,
|
||||
"1-138": true,
|
||||
}
|
||||
|
||||
type ccipQuoteResponse struct {
|
||||
Fee string `json:"fee"`
|
||||
}
|
||||
|
||||
// CCIPProvider implements Provider for Chainlink CCIP
|
||||
type CCIPProvider struct {
|
||||
quoteURL string
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
// NewCCIPProvider creates a new CCIP bridge provider
|
||||
func NewCCIPProvider() *CCIPProvider {
|
||||
quoteURL := os.Getenv("CCIP_ROUTER_QUOTE_URL")
|
||||
return &CCIPProvider{
|
||||
quoteURL: quoteURL,
|
||||
client: &http.Client{
|
||||
Timeout: ccipTimeout,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Name returns the provider name
|
||||
func (p *CCIPProvider) Name() string {
|
||||
return "CCIP"
|
||||
}
|
||||
|
||||
// SupportsRoute returns true for 138 <-> 1
|
||||
func (p *CCIPProvider) SupportsRoute(fromChain, toChain int) bool {
|
||||
key := strconv.Itoa(fromChain) + "-" + strconv.Itoa(toChain)
|
||||
return ccipSupportedPairs[key]
|
||||
}
|
||||
|
||||
// GetQuote returns a bridge quote for 138 <-> 1
|
||||
func (p *CCIPProvider) GetQuote(ctx context.Context, req *BridgeRequest) (*BridgeQuote, error) {
|
||||
if !p.SupportsRoute(req.FromChain, req.ToChain) {
|
||||
return nil, fmt.Errorf("CCIP: unsupported route %d -> %d", req.FromChain, req.ToChain)
|
||||
}
|
||||
|
||||
fee := defaultCCIPFee
|
||||
if p.quoteURL != "" {
|
||||
body, err := json.Marshal(map[string]interface{}{
|
||||
"sourceChain": req.FromChain,
|
||||
"destChain": req.ToChain,
|
||||
"token": req.FromToken,
|
||||
"amount": req.Amount,
|
||||
})
|
||||
if err == nil {
|
||||
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, p.quoteURL, bytes.NewReader(body))
|
||||
if err == nil {
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
resp, err := p.client.Do(httpReq)
|
||||
if err == nil && resp != nil {
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
var r ccipQuoteResponse
|
||||
if json.NewDecoder(resp.Body).Decode(&r) == nil && r.Fee != "" {
|
||||
fee = r.Fee
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &BridgeQuote{
|
||||
Provider: "CCIP",
|
||||
FromChain: req.FromChain,
|
||||
ToChain: req.ToChain,
|
||||
FromAmount: req.Amount,
|
||||
ToAmount: req.Amount,
|
||||
Fee: fee,
|
||||
EstimatedTime: "5-15 min",
|
||||
Route: []BridgeStep{
|
||||
{Provider: "CCIP", From: strconv.Itoa(req.FromChain), To: strconv.Itoa(req.ToChain), Type: "bridge"},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
169
backend/bridge/hop_provider.go
Normal file
169
backend/bridge/hop_provider.go
Normal file
@@ -0,0 +1,169 @@
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
hopAPIBase = "https://api.hop.exchange"
|
||||
hopTimeout = 10 * time.Second
|
||||
)
|
||||
|
||||
// Hop-supported chain IDs: ethereum, optimism, arbitrum, polygon, gnosis, nova, base
|
||||
var hopSupportedChains = map[int]bool{
|
||||
1: true, // ethereum
|
||||
10: true, // optimism
|
||||
42161: true, // arbitrum
|
||||
137: true, // polygon
|
||||
100: true, // gnosis
|
||||
42170: true, // nova
|
||||
8453: true, // base
|
||||
}
|
||||
|
||||
var hopChainIdToSlug = map[int]string{
|
||||
1: "ethereum",
|
||||
10: "optimism",
|
||||
42161: "arbitrum",
|
||||
137: "polygon",
|
||||
100: "gnosis",
|
||||
42170: "nova",
|
||||
8453: "base",
|
||||
}
|
||||
|
||||
// hopQuoteResponse represents Hop API /v1/quote response
|
||||
type hopQuoteResponse struct {
|
||||
AmountIn string `json:"amountIn"`
|
||||
Slippage float64 `json:"slippage"`
|
||||
AmountOutMin string `json:"amountOutMin"`
|
||||
DestinationAmountOutMin string `json:"destinationAmountOutMin"`
|
||||
BonderFee string `json:"bonderFee"`
|
||||
EstimatedReceived string `json:"estimatedReceived"`
|
||||
}
|
||||
|
||||
// HopProvider implements Provider for Hop Protocol
|
||||
type HopProvider struct {
|
||||
apiBase string
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
// NewHopProvider creates a new Hop Protocol bridge provider
|
||||
func NewHopProvider() *HopProvider {
|
||||
return &HopProvider{
|
||||
apiBase: hopAPIBase,
|
||||
client: &http.Client{
|
||||
Timeout: hopTimeout,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Name returns the provider name
|
||||
func (p *HopProvider) Name() string {
|
||||
return "Hop"
|
||||
}
|
||||
|
||||
// SupportsRoute returns true if Hop supports the fromChain->toChain route
|
||||
func (p *HopProvider) SupportsRoute(fromChain, toChain int) bool {
|
||||
return hopSupportedChains[fromChain] && hopSupportedChains[toChain]
|
||||
}
|
||||
|
||||
// GetQuote fetches a bridge quote from the Hop API
|
||||
func (p *HopProvider) GetQuote(ctx context.Context, req *BridgeRequest) (*BridgeQuote, error) {
|
||||
fromSlug, ok := hopChainIdToSlug[req.FromChain]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("Hop: unsupported source chain %d", req.FromChain)
|
||||
}
|
||||
toSlug, ok := hopChainIdToSlug[req.ToChain]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("Hop: unsupported destination chain %d", req.ToChain)
|
||||
}
|
||||
if fromSlug == toSlug {
|
||||
return nil, fmt.Errorf("Hop: source and destination must differ")
|
||||
}
|
||||
|
||||
// Hop token symbols: USDC, USDT, DAI, ETH, MATIC, xDAI
|
||||
params := url.Values{}
|
||||
params.Set("amount", req.Amount)
|
||||
params.Set("token", mapTokenToHop(req.FromToken))
|
||||
params.Set("fromChain", fromSlug)
|
||||
params.Set("toChain", toSlug)
|
||||
params.Set("slippage", "0.5")
|
||||
|
||||
apiURL := fmt.Sprintf("%s/v1/quote?%s", p.apiBase, params.Encode())
|
||||
|
||||
httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, apiURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := p.client.Do(httpReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return nil, fmt.Errorf("Hop API error %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var hopResp hopQuoteResponse
|
||||
if err := json.Unmarshal(body, &hopResp); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse Hop response: %w", err)
|
||||
}
|
||||
|
||||
toAmount := hopResp.EstimatedReceived
|
||||
if toAmount == "" {
|
||||
toAmount = hopResp.AmountIn
|
||||
}
|
||||
|
||||
return &BridgeQuote{
|
||||
Provider: "Hop",
|
||||
FromChain: req.FromChain,
|
||||
ToChain: req.ToChain,
|
||||
FromAmount: req.Amount,
|
||||
ToAmount: toAmount,
|
||||
Fee: hopResp.BonderFee,
|
||||
EstimatedTime: "2-5 min",
|
||||
Route: []BridgeStep{
|
||||
{
|
||||
Provider: "Hop",
|
||||
From: strconv.Itoa(req.FromChain),
|
||||
To: strconv.Itoa(req.ToChain),
|
||||
Type: "bridge",
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// mapTokenToHop maps token address/symbol to Hop token symbol
|
||||
func mapTokenToHop(token string) string {
|
||||
// Common mappings - extend as needed
|
||||
switch token {
|
||||
case "USDC", "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48":
|
||||
return "USDC"
|
||||
case "USDT", "0xdAC17F958D2ee523a2206206994597C13D831ec7":
|
||||
return "USDT"
|
||||
case "DAI", "0x6B175474E89094C44Da98b954EedeAC495271d0F":
|
||||
return "DAI"
|
||||
case "ETH", "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", "0x0000000000000000000000000000000000000000":
|
||||
return "ETH"
|
||||
case "MATIC":
|
||||
return "MATIC"
|
||||
case "xDAI":
|
||||
return "xDAI"
|
||||
default:
|
||||
return "USDC"
|
||||
}
|
||||
}
|
||||
175
backend/bridge/lifi_provider.go
Normal file
175
backend/bridge/lifi_provider.go
Normal file
@@ -0,0 +1,175 @@
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
lifiAPIBase = "https://li.quest"
|
||||
lifiTimeout = 10 * time.Second
|
||||
)
|
||||
|
||||
// LiFi-supported chain IDs for SupportsRoute (subset of Li.Fi's 40+ chains)
|
||||
var lifiSupportedChains = map[int]bool{
|
||||
1: true, // Ethereum Mainnet
|
||||
137: true, // Polygon
|
||||
10: true, // Optimism
|
||||
8453: true, // Base
|
||||
42161: true, // Arbitrum One
|
||||
56: true, // BNB Chain
|
||||
43114: true, // Avalanche
|
||||
100: true, // Gnosis Chain
|
||||
42220: true, // Celo
|
||||
324: true, // zkSync Era
|
||||
59144: true, // Linea
|
||||
5000: true, // Mantle
|
||||
534352: true, // Scroll
|
||||
25: true, // Cronos
|
||||
250: true, // Fantom
|
||||
1111: true, // Wemix
|
||||
}
|
||||
|
||||
// lifiQuoteResponse represents the Li.Fi API quote response structure
|
||||
type lifiQuoteResponse struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Tool string `json:"tool"`
|
||||
Estimate *struct {
|
||||
FromAmount string `json:"fromAmount"`
|
||||
ToAmount string `json:"toAmount"`
|
||||
ToAmountMin string `json:"toAmountMin"`
|
||||
} `json:"estimate"`
|
||||
IncludedSteps []struct {
|
||||
Type string `json:"type"`
|
||||
Tool string `json:"tool"`
|
||||
Estimate *struct {
|
||||
FromAmount string `json:"fromAmount"`
|
||||
ToAmount string `json:"toAmount"`
|
||||
} `json:"estimate"`
|
||||
} `json:"includedSteps"`
|
||||
}
|
||||
|
||||
// LiFiProvider implements Provider for Li.Fi bridge aggregator
|
||||
type LiFiProvider struct {
|
||||
apiBase string
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
// NewLiFiProvider creates a new Li.Fi bridge provider
|
||||
func NewLiFiProvider() *LiFiProvider {
|
||||
return &LiFiProvider{
|
||||
apiBase: lifiAPIBase,
|
||||
client: &http.Client{
|
||||
Timeout: lifiTimeout,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Name returns the provider name
|
||||
func (p *LiFiProvider) Name() string {
|
||||
return "LiFi"
|
||||
}
|
||||
|
||||
// SupportsRoute returns true if Li.Fi supports the fromChain->toChain route
|
||||
func (p *LiFiProvider) SupportsRoute(fromChain, toChain int) bool {
|
||||
return lifiSupportedChains[fromChain] && lifiSupportedChains[toChain]
|
||||
}
|
||||
|
||||
// GetQuote fetches a bridge quote from the Li.Fi API
|
||||
func (p *LiFiProvider) GetQuote(ctx context.Context, req *BridgeRequest) (*BridgeQuote, error) {
|
||||
if req.Recipient == "" {
|
||||
return nil, fmt.Errorf("recipient address required for Li.Fi")
|
||||
}
|
||||
|
||||
params := url.Values{}
|
||||
params.Set("fromChain", strconv.Itoa(req.FromChain))
|
||||
params.Set("toChain", strconv.Itoa(req.ToChain))
|
||||
params.Set("fromToken", req.FromToken)
|
||||
params.Set("toToken", req.ToToken)
|
||||
params.Set("fromAmount", req.Amount)
|
||||
params.Set("fromAddress", req.Recipient)
|
||||
params.Set("toAddress", req.Recipient)
|
||||
params.Set("integrator", "explorer-bridge-aggregator")
|
||||
|
||||
apiURL := fmt.Sprintf("%s/v1/quote?%s", p.apiBase, params.Encode())
|
||||
|
||||
httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, apiURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := p.client.Do(httpReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return nil, fmt.Errorf("Li.Fi API error %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var lifiResp lifiQuoteResponse
|
||||
if err := json.Unmarshal(body, &lifiResp); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse Li.Fi response: %w", err)
|
||||
}
|
||||
|
||||
if lifiResp.Estimate == nil {
|
||||
return nil, fmt.Errorf("Li.Fi response missing estimate")
|
||||
}
|
||||
|
||||
toAmount := lifiResp.Estimate.ToAmount
|
||||
if toAmount == "" && len(lifiResp.IncludedSteps) > 0 && lifiResp.IncludedSteps[len(lifiResp.IncludedSteps)-1].Estimate != nil {
|
||||
toAmount = lifiResp.IncludedSteps[len(lifiResp.IncludedSteps)-1].Estimate.ToAmount
|
||||
}
|
||||
if toAmount == "" {
|
||||
return nil, fmt.Errorf("Li.Fi response missing toAmount")
|
||||
}
|
||||
|
||||
route := make([]BridgeStep, 0, len(lifiResp.IncludedSteps))
|
||||
for _, step := range lifiResp.IncludedSteps {
|
||||
stepType := "bridge"
|
||||
if step.Type == "swap" {
|
||||
stepType = "swap"
|
||||
} else if step.Type == "cross" {
|
||||
stepType = "bridge"
|
||||
}
|
||||
route = append(route, BridgeStep{
|
||||
Provider: step.Tool,
|
||||
From: strconv.Itoa(req.FromChain),
|
||||
To: strconv.Itoa(req.ToChain),
|
||||
Type: stepType,
|
||||
})
|
||||
}
|
||||
if len(route) == 0 {
|
||||
route = append(route, BridgeStep{
|
||||
Provider: lifiResp.Tool,
|
||||
From: strconv.Itoa(req.FromChain),
|
||||
To: strconv.Itoa(req.ToChain),
|
||||
Type: lifiResp.Type,
|
||||
})
|
||||
}
|
||||
|
||||
return &BridgeQuote{
|
||||
Provider: "LiFi",
|
||||
FromChain: req.FromChain,
|
||||
ToChain: req.ToChain,
|
||||
FromAmount: req.Amount,
|
||||
ToAmount: toAmount,
|
||||
Fee: "0",
|
||||
EstimatedTime: "1-5 min",
|
||||
Route: route,
|
||||
}, nil
|
||||
}
|
||||
95
backend/bridge/providers.go
Normal file
95
backend/bridge/providers.go
Normal file
@@ -0,0 +1,95 @@
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Provider interface for bridge providers
|
||||
type Provider interface {
|
||||
GetQuote(ctx context.Context, req *BridgeRequest) (*BridgeQuote, error)
|
||||
Name() string
|
||||
SupportsRoute(fromChain, toChain int) bool
|
||||
}
|
||||
|
||||
// BridgeRequest represents a bridge request
|
||||
type BridgeRequest struct {
|
||||
FromChain int
|
||||
ToChain int
|
||||
FromToken string
|
||||
ToToken string
|
||||
Amount string
|
||||
Recipient string
|
||||
}
|
||||
|
||||
// BridgeQuote represents a bridge quote
|
||||
type BridgeQuote struct {
|
||||
Provider string
|
||||
FromChain int
|
||||
ToChain int
|
||||
FromAmount string
|
||||
ToAmount string
|
||||
Fee string
|
||||
EstimatedTime string
|
||||
Route []BridgeStep
|
||||
}
|
||||
|
||||
// BridgeStep represents a step in bridge route
|
||||
type BridgeStep struct {
|
||||
Provider string
|
||||
From string
|
||||
To string
|
||||
Type string // "bridge" or "swap"
|
||||
}
|
||||
|
||||
// Aggregator aggregates quotes from multiple bridge providers
|
||||
type Aggregator struct {
|
||||
providers []Provider
|
||||
}
|
||||
|
||||
// NewAggregator creates a new bridge aggregator with all providers
|
||||
func NewAggregator() *Aggregator {
|
||||
return &Aggregator{
|
||||
providers: []Provider{
|
||||
NewLiFiProvider(), // Li.Fi: 40+ chains, swap+bridge aggregation
|
||||
NewSocketProvider(), // Socket/Bungee: 40+ chains
|
||||
NewSquidProvider(), // Squid: Axelar-based, 50+ chains
|
||||
NewSymbiosisProvider(), // Symbiosis: 30+ chains
|
||||
NewRelayProvider(), // Relay.link: EVM chains
|
||||
NewStargateProvider(), // Stargate: LayerZero
|
||||
NewCCIPProvider(), // Chainlink CCIP (138 <-> 1)
|
||||
NewHopProvider(), // Hop Protocol (ETH <-> L2)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// GetBestQuote gets the best quote from all providers
|
||||
func (a *Aggregator) GetBestQuote(ctx context.Context, req *BridgeRequest) (*BridgeQuote, error) {
|
||||
var bestQuote *BridgeQuote
|
||||
var bestAmount string
|
||||
|
||||
for _, provider := range a.providers {
|
||||
if !provider.SupportsRoute(req.FromChain, req.ToChain) {
|
||||
continue
|
||||
}
|
||||
|
||||
quote, err := provider.GetQuote(ctx, req)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if bestQuote == nil || quote.ToAmount > bestAmount {
|
||||
bestQuote = quote
|
||||
bestAmount = quote.ToAmount
|
||||
}
|
||||
}
|
||||
|
||||
if bestQuote == nil {
|
||||
return nil, fmt.Errorf("no bridge quotes available")
|
||||
}
|
||||
|
||||
return bestQuote, nil
|
||||
}
|
||||
|
||||
// CCIPProvider is implemented in ccip_provider.go
|
||||
// HopProvider is implemented in hop_provider.go
|
||||
148
backend/bridge/relay_provider.go
Normal file
148
backend/bridge/relay_provider.go
Normal file
@@ -0,0 +1,148 @@
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
relayAPIBase = "https://api.relay.link"
|
||||
relayTimeout = 10 * time.Second
|
||||
)
|
||||
|
||||
// Relay-supported chain IDs (EVM chains, configurable)
|
||||
var relaySupportedChains = map[int]bool{
|
||||
1: true, // Ethereum
|
||||
10: true, // Optimism
|
||||
137: true, // Polygon
|
||||
42161: true, // Arbitrum
|
||||
8453: true, // Base
|
||||
56: true, // BNB Chain
|
||||
43114: true, // Avalanche
|
||||
100: true, // Gnosis
|
||||
25: true, // Cronos
|
||||
324: true, // zkSync
|
||||
59144: true, // Linea
|
||||
534352: true, // Scroll
|
||||
}
|
||||
|
||||
type relayQuoteRequest struct {
|
||||
User string `json:"user"`
|
||||
OriginChainID int `json:"originChainId"`
|
||||
DestinationChainID int `json:"destinationChainId"`
|
||||
OriginCurrency string `json:"originCurrency"`
|
||||
DestinationCurrency string `json:"destinationCurrency"`
|
||||
Amount string `json:"amount"`
|
||||
TradeType string `json:"tradeType"`
|
||||
Recipient string `json:"recipient,omitempty"`
|
||||
}
|
||||
|
||||
type relayQuoteResponse struct {
|
||||
Details *struct {
|
||||
CurrencyOut *struct {
|
||||
Amount string `json:"amount"`
|
||||
} `json:"currencyOut"`
|
||||
} `json:"details"`
|
||||
}
|
||||
|
||||
// RelayProvider implements Provider for Relay.link
|
||||
type RelayProvider struct {
|
||||
apiBase string
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
// NewRelayProvider creates a new Relay.link bridge provider
|
||||
func NewRelayProvider() *RelayProvider {
|
||||
return &RelayProvider{
|
||||
apiBase: relayAPIBase,
|
||||
client: &http.Client{
|
||||
Timeout: relayTimeout,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Name returns the provider name
|
||||
func (p *RelayProvider) Name() string {
|
||||
return "Relay"
|
||||
}
|
||||
|
||||
// SupportsRoute returns true if Relay supports the fromChain->toChain route
|
||||
func (p *RelayProvider) SupportsRoute(fromChain, toChain int) bool {
|
||||
return relaySupportedChains[fromChain] && relaySupportedChains[toChain]
|
||||
}
|
||||
|
||||
// GetQuote fetches a bridge quote from the Relay API
|
||||
func (p *RelayProvider) GetQuote(ctx context.Context, req *BridgeRequest) (*BridgeQuote, error) {
|
||||
if req.Recipient == "" {
|
||||
return nil, fmt.Errorf("Relay: recipient address required")
|
||||
}
|
||||
|
||||
bodyReq := relayQuoteRequest{
|
||||
User: req.Recipient,
|
||||
OriginChainID: req.FromChain,
|
||||
DestinationChainID: req.ToChain,
|
||||
OriginCurrency: req.FromToken,
|
||||
DestinationCurrency: req.ToToken,
|
||||
Amount: req.Amount,
|
||||
TradeType: "EXACT_INPUT",
|
||||
Recipient: req.Recipient,
|
||||
}
|
||||
jsonBody, err := json.Marshal(bodyReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
apiURL := p.apiBase + "/quote/v2"
|
||||
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, apiURL, bytes.NewReader(jsonBody))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := p.client.Do(httpReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("Relay API error %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
var relayResp relayQuoteResponse
|
||||
if err := json.Unmarshal(body, &relayResp); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse Relay response: %w", err)
|
||||
}
|
||||
|
||||
toAmount := ""
|
||||
if relayResp.Details != nil && relayResp.Details.CurrencyOut != nil {
|
||||
toAmount = relayResp.Details.CurrencyOut.Amount
|
||||
}
|
||||
if toAmount == "" {
|
||||
return nil, fmt.Errorf("Relay: no quote amount")
|
||||
}
|
||||
|
||||
steps := []BridgeStep{{Provider: "Relay", From: strconv.Itoa(req.FromChain), To: strconv.Itoa(req.ToChain), Type: "bridge"}}
|
||||
|
||||
return &BridgeQuote{
|
||||
Provider: "Relay",
|
||||
FromChain: req.FromChain,
|
||||
ToChain: req.ToChain,
|
||||
FromAmount: req.Amount,
|
||||
ToAmount: toAmount,
|
||||
Fee: "0",
|
||||
EstimatedTime: "1-5 min",
|
||||
Route: steps,
|
||||
}, nil
|
||||
}
|
||||
92
backend/bridge/socket_provider.go
Normal file
92
backend/bridge/socket_provider.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
socketAPIBase = "https://public-backend.bungee.exchange"
|
||||
socketTimeout = 10 * time.Second
|
||||
)
|
||||
|
||||
var socketSupportedChains = map[int]bool{
|
||||
1: true, 10: true, 137: true, 42161: true, 8453: true,
|
||||
56: true, 43114: true, 100: true, 25: true, 250: true,
|
||||
324: true, 59144: true, 534352: true, 42220: true, 5000: true, 1111: true,
|
||||
}
|
||||
|
||||
type socketQuoteResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Result *struct {
|
||||
Route *struct {
|
||||
ToAmount string `json:"toAmount"`
|
||||
ToAmountMin string `json:"toAmountMin"`
|
||||
} `json:"route"`
|
||||
} `json:"result"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type SocketProvider struct {
|
||||
apiBase string
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func NewSocketProvider() *SocketProvider {
|
||||
return &SocketProvider{apiBase: socketAPIBase, client: &http.Client{Timeout: socketTimeout}}
|
||||
}
|
||||
|
||||
func (p *SocketProvider) Name() string { return "Socket" }
|
||||
|
||||
func (p *SocketProvider) SupportsRoute(fromChain, toChain int) bool {
|
||||
return socketSupportedChains[fromChain] && socketSupportedChains[toChain]
|
||||
}
|
||||
|
||||
func (p *SocketProvider) GetQuote(ctx context.Context, req *BridgeRequest) (*BridgeQuote, error) {
|
||||
if req.Recipient == "" {
|
||||
return nil, fmt.Errorf("Socket: recipient required")
|
||||
}
|
||||
params := url.Values{}
|
||||
params.Set("fromChainId", strconv.Itoa(req.FromChain))
|
||||
params.Set("toChainId", strconv.Itoa(req.ToChain))
|
||||
params.Set("fromTokenAddress", req.FromToken)
|
||||
params.Set("toTokenAddress", req.ToToken)
|
||||
params.Set("fromAmount", req.Amount)
|
||||
params.Set("recipient", req.Recipient)
|
||||
apiURL := fmt.Sprintf("%s/api/v1/bungee/quote?%s", p.apiBase, params.Encode())
|
||||
httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, apiURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := p.client.Do(httpReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
var r socketQuoteResponse
|
||||
if err := json.Unmarshal(body, &r); err != nil {
|
||||
return nil, fmt.Errorf("Socket parse error: %w", err)
|
||||
}
|
||||
if !r.Success || r.Result == nil || r.Result.Route == nil {
|
||||
return nil, fmt.Errorf("Socket API: %s", r.Message)
|
||||
}
|
||||
toAmount := r.Result.Route.ToAmount
|
||||
if toAmount == "" {
|
||||
toAmount = r.Result.Route.ToAmountMin
|
||||
}
|
||||
if toAmount == "" {
|
||||
return nil, fmt.Errorf("Socket: no amount")
|
||||
}
|
||||
steps := []BridgeStep{{Provider: "Socket", From: strconv.Itoa(req.FromChain), To: strconv.Itoa(req.ToChain), Type: "bridge"}}
|
||||
return &BridgeQuote{
|
||||
Provider: "Socket", FromChain: req.FromChain, ToChain: req.ToChain,
|
||||
FromAmount: req.Amount, ToAmount: toAmount, Fee: "0", EstimatedTime: "1-5 min", Route: steps,
|
||||
}, nil
|
||||
}
|
||||
106
backend/bridge/squid_provider.go
Normal file
106
backend/bridge/squid_provider.go
Normal file
@@ -0,0 +1,106 @@
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
squidAPIBase = "https://v2.api.squidrouter.com"
|
||||
squidTimeout = 10 * time.Second
|
||||
squidIntegrator = "explorer-bridge-aggregator"
|
||||
)
|
||||
|
||||
var squidSupportedChains = map[int]bool{
|
||||
1: true, 10: true, 137: true, 42161: true, 8453: true,
|
||||
56: true, 43114: true, 100: true, 25: true, 250: true,
|
||||
324: true, 59144: true, 534352: true, 42220: true, 5000: true, 1111: true,
|
||||
}
|
||||
|
||||
type squidReq struct {
|
||||
FromAddress string `json:"fromAddress"`
|
||||
FromChain string `json:"fromChain"`
|
||||
FromToken string `json:"fromToken"`
|
||||
FromAmount string `json:"fromAmount"`
|
||||
ToChain string `json:"toChain"`
|
||||
ToToken string `json:"toToken"`
|
||||
ToAddress string `json:"toAddress"`
|
||||
Slippage int `json:"slippage"`
|
||||
}
|
||||
|
||||
type squidResp struct {
|
||||
Route *struct {
|
||||
Estimate *struct {
|
||||
ToAmount string `json:"toAmount"`
|
||||
ToAmountMin string `json:"toAmountMin"`
|
||||
} `json:"estimate"`
|
||||
} `json:"route"`
|
||||
}
|
||||
|
||||
type SquidProvider struct {
|
||||
apiBase string
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func NewSquidProvider() *SquidProvider {
|
||||
return &SquidProvider{apiBase: squidAPIBase, client: &http.Client{Timeout: squidTimeout}}
|
||||
}
|
||||
|
||||
func (p *SquidProvider) Name() string { return "Squid" }
|
||||
|
||||
func (p *SquidProvider) SupportsRoute(fromChain, toChain int) bool {
|
||||
return squidSupportedChains[fromChain] && squidSupportedChains[toChain]
|
||||
}
|
||||
|
||||
func (p *SquidProvider) GetQuote(ctx context.Context, req *BridgeRequest) (*BridgeQuote, error) {
|
||||
addr := req.Recipient
|
||||
if addr == "" {
|
||||
addr = "0x0000000000000000000000000000000000000000"
|
||||
}
|
||||
bodyReq := squidReq{
|
||||
FromAddress: addr, FromChain: strconv.Itoa(req.FromChain), FromToken: req.FromToken,
|
||||
FromAmount: req.Amount, ToChain: strconv.Itoa(req.ToChain), ToToken: req.ToToken,
|
||||
ToAddress: addr, Slippage: 1,
|
||||
}
|
||||
jsonBody, _ := json.Marshal(bodyReq)
|
||||
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, p.apiBase+"/v2/route", bytes.NewReader(jsonBody))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
httpReq.Header.Set("x-integrator-id", squidIntegrator)
|
||||
resp, err := p.client.Do(httpReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("Squid API %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
var r squidResp
|
||||
if err := json.Unmarshal(body, &r); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if r.Route == nil || r.Route.Estimate == nil {
|
||||
return nil, fmt.Errorf("Squid: no route")
|
||||
}
|
||||
toAmount := r.Route.Estimate.ToAmount
|
||||
if toAmount == "" {
|
||||
toAmount = r.Route.Estimate.ToAmountMin
|
||||
}
|
||||
if toAmount == "" {
|
||||
return nil, fmt.Errorf("Squid: no amount")
|
||||
}
|
||||
return &BridgeQuote{
|
||||
Provider: "Squid", FromChain: req.FromChain, ToChain: req.ToChain,
|
||||
FromAmount: req.Amount, ToAmount: toAmount, Fee: "0", EstimatedTime: "1-5 min",
|
||||
Route: []BridgeStep{{Provider: "Squid", From: strconv.Itoa(req.FromChain), To: strconv.Itoa(req.ToChain), Type: "bridge"}},
|
||||
}, nil
|
||||
}
|
||||
178
backend/bridge/stargate_provider.go
Normal file
178
backend/bridge/stargate_provider.go
Normal file
@@ -0,0 +1,178 @@
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
stargateAPIBase = "https://stargate.finance/api/v1"
|
||||
stargateTimeout = 10 * time.Second
|
||||
)
|
||||
|
||||
// chainIDToStargateKey maps chain ID to Stargate chain key
|
||||
var stargateChainKeys = map[int]string{
|
||||
1: "ethereum",
|
||||
10: "optimism",
|
||||
137: "polygon",
|
||||
42161: "arbitrum",
|
||||
8453: "base",
|
||||
56: "bnb",
|
||||
43114: "avalanche",
|
||||
25: "cronos",
|
||||
100: "gnosis",
|
||||
324: "zksync",
|
||||
59144: "linea",
|
||||
534352: "scroll",
|
||||
}
|
||||
|
||||
// Stargate-supported chain IDs
|
||||
var stargateSupportedChains = map[int]bool{
|
||||
1: true,
|
||||
10: true,
|
||||
137: true,
|
||||
42161: true,
|
||||
8453: true,
|
||||
56: true,
|
||||
43114: true,
|
||||
25: true,
|
||||
100: true,
|
||||
324: true,
|
||||
59144: true,
|
||||
534352: true,
|
||||
}
|
||||
|
||||
type stargateQuoteResponse struct {
|
||||
Quotes []struct {
|
||||
Bridge string `json:"bridge"`
|
||||
SrcAmount string `json:"srcAmount"`
|
||||
DstAmount string `json:"dstAmount"`
|
||||
DstAmountMin string `json:"dstAmountMin"`
|
||||
Error string `json:"error"`
|
||||
Duration *struct {
|
||||
Estimated int `json:"estimated"`
|
||||
} `json:"duration"`
|
||||
} `json:"quotes"`
|
||||
}
|
||||
|
||||
// StargateProvider implements Provider for Stargate (LayerZero)
|
||||
type StargateProvider struct {
|
||||
apiBase string
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
// NewStargateProvider creates a new Stargate bridge provider
|
||||
func NewStargateProvider() *StargateProvider {
|
||||
return &StargateProvider{
|
||||
apiBase: stargateAPIBase,
|
||||
client: &http.Client{
|
||||
Timeout: stargateTimeout,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Name returns the provider name
|
||||
func (p *StargateProvider) Name() string {
|
||||
return "Stargate"
|
||||
}
|
||||
|
||||
// SupportsRoute returns true if Stargate supports the fromChain->toChain route
|
||||
func (p *StargateProvider) SupportsRoute(fromChain, toChain int) bool {
|
||||
return stargateSupportedChains[fromChain] && stargateSupportedChains[toChain]
|
||||
}
|
||||
|
||||
// GetQuote fetches a bridge quote from the Stargate API
|
||||
func (p *StargateProvider) GetQuote(ctx context.Context, req *BridgeRequest) (*BridgeQuote, error) {
|
||||
srcKey, ok := stargateChainKeys[req.FromChain]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("Stargate: unsupported fromChain %d", req.FromChain)
|
||||
}
|
||||
dstKey, ok := stargateChainKeys[req.ToChain]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("Stargate: unsupported toChain %d", req.ToChain)
|
||||
}
|
||||
|
||||
if req.Recipient == "" {
|
||||
req.Recipient = "0x0000000000000000000000000000000000000000"
|
||||
}
|
||||
|
||||
params := url.Values{}
|
||||
params.Set("srcToken", req.FromToken)
|
||||
params.Set("dstToken", req.ToToken)
|
||||
params.Set("srcChainKey", srcKey)
|
||||
params.Set("dstChainKey", dstKey)
|
||||
params.Set("srcAddress", req.Recipient)
|
||||
params.Set("dstAddress", req.Recipient)
|
||||
params.Set("srcAmount", req.Amount)
|
||||
params.Set("dstAmountMin", "0")
|
||||
|
||||
apiURL := fmt.Sprintf("%s/quotes?%s", p.apiBase, params.Encode())
|
||||
|
||||
httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, apiURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := p.client.Do(httpReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("Stargate API error %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
var stargateResp stargateQuoteResponse
|
||||
if err := json.Unmarshal(body, &stargateResp); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse Stargate response: %w", err)
|
||||
}
|
||||
|
||||
var bestIdx = -1
|
||||
for i := range stargateResp.Quotes {
|
||||
q := &stargateResp.Quotes[i]
|
||||
if q.Error != "" {
|
||||
continue
|
||||
}
|
||||
if bestIdx < 0 || q.DstAmount > stargateResp.Quotes[bestIdx].DstAmount {
|
||||
bestIdx = i
|
||||
}
|
||||
}
|
||||
|
||||
if bestIdx < 0 {
|
||||
return nil, fmt.Errorf("Stargate: no valid quotes")
|
||||
}
|
||||
bestQuote := &stargateResp.Quotes[bestIdx]
|
||||
|
||||
estTime := "1-5 min"
|
||||
if bestQuote.Duration != nil && bestQuote.Duration.Estimated > 0 {
|
||||
estTime = fmt.Sprintf("%d sec", bestQuote.Duration.Estimated)
|
||||
}
|
||||
|
||||
return &BridgeQuote{
|
||||
Provider: "Stargate",
|
||||
FromChain: req.FromChain,
|
||||
ToChain: req.ToChain,
|
||||
FromAmount: req.Amount,
|
||||
ToAmount: bestQuote.DstAmount,
|
||||
Fee: "0",
|
||||
EstimatedTime: estTime,
|
||||
Route: []BridgeStep{{
|
||||
Provider: bestQuote.Bridge,
|
||||
From: strconv.Itoa(req.FromChain),
|
||||
To: strconv.Itoa(req.ToChain),
|
||||
Type: "bridge",
|
||||
}},
|
||||
}, nil
|
||||
}
|
||||
95
backend/bridge/symbiosis_provider.go
Normal file
95
backend/bridge/symbiosis_provider.go
Normal file
@@ -0,0 +1,95 @@
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
symbiosisAPIBase = "https://api.symbiosis.finance/crosschain"
|
||||
symbiosisTimeout = 10 * time.Second
|
||||
)
|
||||
|
||||
var symbiosisSupportedChains = map[int]bool{
|
||||
1: true, 10: true, 137: true, 42161: true, 8453: true,
|
||||
56: true, 43114: true, 100: true, 25: true, 250: true,
|
||||
324: true, 59144: true, 534352: true, 42220: true, 5000: true,
|
||||
}
|
||||
|
||||
type symbiosisReq struct {
|
||||
Amount string `json:"amount"`
|
||||
TokenInChain int `json:"tokenInChainId"`
|
||||
TokenIn string `json:"tokenIn"`
|
||||
TokenOutChain int `json:"tokenOutChainId"`
|
||||
TokenOut string `json:"tokenOut"`
|
||||
From string `json:"from"`
|
||||
Slippage int `json:"slippage"`
|
||||
}
|
||||
|
||||
type symbiosisResp struct {
|
||||
AmountOut string `json:"amountOut"`
|
||||
AmountOutMin string `json:"amountOutMin"`
|
||||
}
|
||||
|
||||
type SymbiosisProvider struct {
|
||||
apiBase string
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func NewSymbiosisProvider() *SymbiosisProvider {
|
||||
return &SymbiosisProvider{apiBase: symbiosisAPIBase, client: &http.Client{Timeout: symbiosisTimeout}}
|
||||
}
|
||||
|
||||
func (p *SymbiosisProvider) Name() string { return "Symbiosis" }
|
||||
|
||||
func (p *SymbiosisProvider) SupportsRoute(fromChain, toChain int) bool {
|
||||
return symbiosisSupportedChains[fromChain] && symbiosisSupportedChains[toChain]
|
||||
}
|
||||
|
||||
func (p *SymbiosisProvider) GetQuote(ctx context.Context, req *BridgeRequest) (*BridgeQuote, error) {
|
||||
addr := req.Recipient
|
||||
if addr == "" {
|
||||
addr = "0x0000000000000000000000000000000000000000"
|
||||
}
|
||||
bodyReq := symbiosisReq{
|
||||
Amount: req.Amount, TokenInChain: req.FromChain, TokenIn: req.FromToken,
|
||||
TokenOutChain: req.ToChain, TokenOut: req.ToToken, From: addr, Slippage: 100,
|
||||
}
|
||||
jsonBody, _ := json.Marshal(bodyReq)
|
||||
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, p.apiBase+"/v2/quote", bytes.NewReader(jsonBody))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
resp, err := p.client.Do(httpReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("Symbiosis API %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
var r symbiosisResp
|
||||
if err := json.Unmarshal(body, &r); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
toAmount := r.AmountOut
|
||||
if toAmount == "" {
|
||||
toAmount = r.AmountOutMin
|
||||
}
|
||||
if toAmount == "" {
|
||||
return nil, fmt.Errorf("Symbiosis: no amount")
|
||||
}
|
||||
return &BridgeQuote{
|
||||
Provider: "Symbiosis", FromChain: req.FromChain, ToChain: req.ToChain,
|
||||
FromAmount: req.Amount, ToAmount: toAmount, Fee: "0", EstimatedTime: "1-5 min",
|
||||
Route: []BridgeStep{{Provider: "Symbiosis", From: strconv.Itoa(req.FromChain), To: strconv.Itoa(req.ToChain), Type: "bridge"}},
|
||||
}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user