149 lines
3.7 KiB
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
|
|
}
|