internal/app/wallet/toncenter.go
Rama main · réplica de lectura del repositorio original.
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
}