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" } }