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 }