Files
explorer-monorepo/backend/bridge/stargate_provider.go

179 lines
4.2 KiB
Go

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
}