package tron import ( "bytes" "encoding/hex" "encoding/json" "errors" "io" "math/rand" "net/http" "os" "strings" "time" ) // defaultAPIKeys are the public TronGrid API keys used by tronpy when the // endpoint is a TronGrid host and no explicit key is supplied. var defaultAPIKeys = []string{ "f92221d5-7056-4366-b96f-65d3662ec2d9", "1e0a625f-cfa5-43ee-ba41-a09db1aae55f", "f399168e-2259-481c-90fc-6b3d984c5463", "da63253b-aa9c-46e7-a4e8-22d259a8026d", "88c10958-af7b-4d5a-8eef-6e84bf5fb809", "169bb4b3-cbe8-449a-984e-80e9adacac55", } // HTTPProvider is a minimal JSON-over-HTTP client for a TRON fullnode. It // mirrors tronpy.providers.HTTPProvider: POST {} with a JSON body and // an optional Tron-Pro-Api-Key header for TronGrid endpoints. type HTTPProvider struct { endpointURI string timeout time.Duration useAPIKey bool apiKeys []string client *http.Client } // NewHTTPProvider creates a provider for the given endpoint URI. When the // endpoint is a TronGrid host an api key is used (the supplied one, or the // public defaults). An empty endpoint falls back to the TRONPY_HTTP_PROVIDER_URI // environment variable and then to api.trongrid.io. func NewHTTPProvider(endpointURI string, timeout time.Duration, apiKey string) *HTTPProvider { if endpointURI == "" { endpointURI = os.Getenv("TRONPY_HTTP_PROVIDER_URI") } if endpointURI == "" { endpointURI = "https://api.trongrid.io/" } p := &HTTPProvider{ endpointURI: strings.TrimRight(endpointURI, "/"), timeout: timeout, client: &http.Client{Timeout: timeout}, } if strings.Contains(p.endpointURI, "trongrid") { p.useAPIKey = true if apiKey != "" { p.apiKeys = []string{apiKey} } else { p.apiKeys = append([]string{}, defaultAPIKeys...) } } return p } // Endpoint returns the configured endpoint URI. func (p *HTTPProvider) Endpoint() string { return p.endpointURI } // makeRequest issues a POST to {endpoint}/{method} with the JSON params. func (p *HTTPProvider) makeRequest(method string, params any) (map[string]any, error) { if params == nil { params = map[string]any{} } body, err := json.Marshal(params) if err != nil { return nil, err } for attempt := 0; ; attempt++ { req, err := http.NewRequest(http.MethodPost, p.endpointURI+"/"+method, bytes.NewReader(body)) if err != nil { return nil, err } req.Header.Set("Content-Type", "application/json") req.Header.Set("User-Agent", "kisstron") if p.useAPIKey && len(p.apiKeys) > 0 { req.Header.Set("Tron-Pro-Api-Key", p.apiKeys[rand.Intn(len(p.apiKeys))]) } resp, err := p.client.Do(req) if err != nil { return nil, err } data, readErr := io.ReadAll(resp.Body) resp.Body.Close() if readErr != nil { return nil, readErr } if resp.StatusCode == http.StatusForbidden && strings.Contains(string(data), "Exceed the user daily usage") { if !p.rotateAPIKey() || attempt >= len(defaultAPIKeys) { return nil, errors.New("rate limit! please add more API keys") } continue } if resp.StatusCode < 200 || resp.StatusCode >= 300 { return nil, errors.New("http error: " + resp.Status) } dec := json.NewDecoder(bytes.NewReader(data)) dec.UseNumber() var out map[string]any if err := dec.Decode(&out); err != nil { return nil, err } return out, nil } } func (p *HTTPProvider) rotateAPIKey() bool { if len(p.apiKeys) == 0 { return false } p.apiKeys = p.apiKeys[1:] return len(p.apiKeys) > 0 } // handleAPIError maps a node error payload to the same typed errors as // tronpy. It returns nil when the payload indicates success. func handleAPIError(payload map[string]any) error { if res, ok := payload["result"].(bool); ok && res { return nil } if e, ok := payload["Error"].(string); ok { return errors.New(e) } if code, ok := payload["code"].(string); ok { msg := code if m, ok := payload["message"].(string); ok { if decoded, err := hex.DecodeString(m); err == nil { msg = string(decoded) } else { msg = m } } switch code { case "SIGERROR": return wrapErr(ErrBadSignature, msg) case "TAPOS_ERROR": return wrapErr(ErrTapos, msg) case "TRANSACTION_EXPIRATION_ERROR", "TOO_BIG_TRANSACTION_ERROR": return wrapErr(ErrTransaction, msg) case "CONTRACT_VALIDATE_ERROR": return wrapErr(ErrValidation, msg) default: return wrapErr(ErrAPI, msg) } } if res, ok := payload["result"].(map[string]any); ok { return handleAPIError(res) } return nil }