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

149 lines
3.7 KiB
Go

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
}