Ctrl K
ComunidadGuías…

internal/app/wallet/toncenter.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package wallet

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"path"
	"strings"
	"time"
)

const (
	MaxProviderRequest  = 4 << 20
	MaxProviderResponse = 16 << 20
)

var (
	ErrProviderRequest     = errors.New("invalid wallet provider request")
	ErrProviderResponse    = errors.New("invalid wallet provider response")
	ErrProviderUnavailable = errors.New("wallet provider unavailable")
)

// ProviderHTTPError carries only the upstream status. URLs, credentials and
// bodies are intentionally excluded from errors that may be logged by RPC.
type ProviderHTTPError struct{ Status int }

func (e *ProviderHTTPError) Error() string {
	return fmt.Sprintf("wallet provider HTTP status %d", e.Status)
}

// Toncenter implements the transport of toncenter.performApiRequest. It never
// retries: the same endpoint can submit a signed transfer as well as read data.
type Toncenter struct {
	client *http.Client
	apiKey string
}

func NewToncenter(apiKey string) *Toncenter {
	transport := http.DefaultTransport.(*http.Transport).Clone()
	transport.Proxy = nil
	transport.MaxConnsPerHost = 16
	transport.MaxIdleConnsPerHost = 16
	transport.ResponseHeaderTimeout = 15 * time.Second
	return &Toncenter{client: &http.Client{
		Transport: transport, Timeout: 30 * time.Second,
		CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse },
	}, apiKey: apiKey}
}

// Perform accepts a path, never a caller-selected origin. A nil payload selects
// GET; a present payload selects POST, preserving the TL optional distinction.
func (p *Toncenter) Perform(ctx context.Context, endpoint string, query, payload *string) (string, error) {
	if p == nil || p.client == nil {
		return "", ErrProviderUnavailable
	}
	if len(endpoint) > 2048 || len(endpoint) == 0 || endpoint[0] != '/' || strings.ContainsAny(endpoint, "\\%?#\r\n\x00") || path.Clean(endpoint) != endpoint {
		return "", ErrProviderRequest
	}
	if !strings.HasPrefix(endpoint, "/api/v2/") && !strings.HasPrefix(endpoint, "/api/v3/") {
		return "", ErrProviderRequest
	}
	u := url.URL{Scheme: "https", Host: "toncenter.com", Path: endpoint}
	if query != nil {
		if len(*query) > 64<<10 || strings.ContainsAny(*query, "\r\n\x00#") {
			return "", ErrProviderRequest
		}
		values, err := url.ParseQuery(*query)
		if err != nil {
			return "", ErrProviderRequest
		}
		// Provider credentials belong to server configuration, not request input.
		for k := range values {
			if strings.EqualFold(k, "api_key") || strings.EqualFold(k, "x-api-key") {
				return "", ErrProviderRequest
			}
		}
		u.RawQuery = *query
	}
	method := http.MethodGet
	var body io.Reader
	if payload != nil {
		if len(*payload) > MaxProviderRequest || !json.Valid([]byte(*payload)) {
			return "", ErrProviderRequest
		}
		method = http.MethodPost
		body = strings.NewReader(*payload)
	}
	req, err := http.NewRequestWithContext(ctx, method, u.String(), body)
	if err != nil {
		return "", ErrProviderRequest
	}
	req.Header.Set("Accept", "application/json")
	if payload != nil {
		req.Header.Set("Content-Type", "application/json")
	}
	if p.apiKey != "" {
		req.Header.Set("X-API-Key", p.apiKey)
	}
	resp, err := p.client.Do(req)
	if err != nil {
		if ctx.Err() != nil {
			return "", ctx.Err()
		}
		return "", ErrProviderUnavailable
	}
	defer resp.Body.Close()
	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		return "", &ProviderHTTPError{Status: resp.StatusCode}
	}
	if resp.ContentLength > MaxProviderResponse {
		return "", ErrProviderResponse
	}
	data, err := io.ReadAll(io.LimitReader(resp.Body, MaxProviderResponse+1))
	if err != nil || len(data) > MaxProviderResponse || !json.Valid(data) {
		return "", ErrProviderResponse
	}
	// Preserve JSON-RPC errors and IDs verbatim. HTTP success does not imply a
	// successful JSON-RPC result or a confirmed blockchain transaction.
	return string(data), nil
}