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 }