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

170 lines
4.2 KiB
Go

package bridge
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"time"
)
const (
hopAPIBase = "https://api.hop.exchange"
hopTimeout = 10 * time.Second
)
// Hop-supported chain IDs: ethereum, optimism, arbitrum, polygon, gnosis, nova, base
var hopSupportedChains = map[int]bool{
1: true, // ethereum
10: true, // optimism
42161: true, // arbitrum
137: true, // polygon
100: true, // gnosis
42170: true, // nova
8453: true, // base
}
var hopChainIdToSlug = map[int]string{
1: "ethereum",
10: "optimism",
42161: "arbitrum",
137: "polygon",
100: "gnosis",
42170: "nova",
8453: "base",
}
// hopQuoteResponse represents Hop API /v1/quote response
type hopQuoteResponse struct {
AmountIn string `json:"amountIn"`
Slippage float64 `json:"slippage"`
AmountOutMin string `json:"amountOutMin"`
DestinationAmountOutMin string `json:"destinationAmountOutMin"`
BonderFee string `json:"bonderFee"`
EstimatedReceived string `json:"estimatedReceived"`
}
// HopProvider implements Provider for Hop Protocol
type HopProvider struct {
apiBase string
client *http.Client
}
// NewHopProvider creates a new Hop Protocol bridge provider
func NewHopProvider() *HopProvider {
return &HopProvider{
apiBase: hopAPIBase,
client: &http.Client{
Timeout: hopTimeout,
},
}
}
// Name returns the provider name
func (p *HopProvider) Name() string {
return "Hop"
}
// SupportsRoute returns true if Hop supports the fromChain->toChain route
func (p *HopProvider) SupportsRoute(fromChain, toChain int) bool {
return hopSupportedChains[fromChain] && hopSupportedChains[toChain]
}
// GetQuote fetches a bridge quote from the Hop API
func (p *HopProvider) GetQuote(ctx context.Context, req *BridgeRequest) (*BridgeQuote, error) {
fromSlug, ok := hopChainIdToSlug[req.FromChain]
if !ok {
return nil, fmt.Errorf("Hop: unsupported source chain %d", req.FromChain)
}
toSlug, ok := hopChainIdToSlug[req.ToChain]
if !ok {
return nil, fmt.Errorf("Hop: unsupported destination chain %d", req.ToChain)
}
if fromSlug == toSlug {
return nil, fmt.Errorf("Hop: source and destination must differ")
}
// Hop token symbols: USDC, USDT, DAI, ETH, MATIC, xDAI
params := url.Values{}
params.Set("amount", req.Amount)
params.Set("token", mapTokenToHop(req.FromToken))
params.Set("fromChain", fromSlug)
params.Set("toChain", toSlug)
params.Set("slippage", "0.5")
apiURL := fmt.Sprintf("%s/v1/quote?%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()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("Hop API error %d: %s", resp.StatusCode, string(body))
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var hopResp hopQuoteResponse
if err := json.Unmarshal(body, &hopResp); err != nil {
return nil, fmt.Errorf("failed to parse Hop response: %w", err)
}
toAmount := hopResp.EstimatedReceived
if toAmount == "" {
toAmount = hopResp.AmountIn
}
return &BridgeQuote{
Provider: "Hop",
FromChain: req.FromChain,
ToChain: req.ToChain,
FromAmount: req.Amount,
ToAmount: toAmount,
Fee: hopResp.BonderFee,
EstimatedTime: "2-5 min",
Route: []BridgeStep{
{
Provider: "Hop",
From: strconv.Itoa(req.FromChain),
To: strconv.Itoa(req.ToChain),
Type: "bridge",
},
},
}, nil
}
// mapTokenToHop maps token address/symbol to Hop token symbol
func mapTokenToHop(token string) string {
// Common mappings - extend as needed
switch token {
case "USDC", "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48":
return "USDC"
case "USDT", "0xdAC17F958D2ee523a2206206994597C13D831ec7":
return "USDT"
case "DAI", "0x6B175474E89094C44Da98b954EedeAC495271d0F":
return "DAI"
case "ETH", "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", "0x0000000000000000000000000000000000000000":
return "ETH"
case "MATIC":
return "MATIC"
case "xDAI":
return "xDAI"
default:
return "USDC"
}
}