170 lines
4.2 KiB
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"
|
|
}
|
|
}
|