Add full monorepo: virtual-banker, backend, frontend, docs, scripts, deployment

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
defiQUG
2026-02-10 11:32:49 -08:00
parent aafcd913c2
commit 88bc76da91
815 changed files with 125522 additions and 264 deletions

View 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
}

View 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"
}
}

View 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
}

View 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

View 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
}

View 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
}

View 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
}

View 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
}

View 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
}