Files
dragonchain-prime-sdk-go/client/client.go
Andrew Miller eb6100b736 Add context.Context parameter to all API methods
Enable request timeout and cancellation control by adding context.Context
as the first parameter to all SDK API methods. This allows users to:
- Set per-request timeouts
- Cancel in-flight requests
- Pass request-scoped values
2025-12-29 11:00:13 -05:00

146 lines
3.6 KiB
Go

package client
import (
"bytes"
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"
)
type Client struct {
publicID string
authKeyID string
authKey string
baseURL string
httpClient *http.Client
}
func NewClient(publicID, authKeyID, authKey, baseURL string) *Client {
return &Client{
publicID: publicID,
authKeyID: authKeyID,
authKey: authKey,
baseURL: strings.TrimSuffix(baseURL, "/"),
httpClient: &http.Client{
Timeout: 30 * time.Second,
},
}
}
func (c *Client) generateAuthHeader(method, path, timestamp, contentType string, body []byte) string {
msgStr := c.hmacMessage(method, path, timestamp, contentType, body)
hmacMsg := c.createHmac(msgStr)
b64hmac := base64.StdEncoding.EncodeToString(hmacMsg)
return fmt.Sprintf("DC1-HMAC-SHA256 %s:%s", c.authKeyID, b64hmac)
}
func (c *Client) hmacMessage(method, path, timestamp, contentType string, body []byte) string {
h := sha256.New()
h.Write(body)
hashContent := h.Sum(nil)
b64Content := base64.StdEncoding.EncodeToString(hashContent)
return fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s",
strings.ToUpper(method),
path,
c.publicID,
timestamp,
contentType,
b64Content,
)
}
func (c *Client) createHmac(msgStr string) []byte {
h := hmac.New(sha256.New, []byte(c.authKey))
h.Write([]byte(msgStr))
return h.Sum(nil)
}
func (c *Client) doRequest(ctx context.Context, method, path, contentType string, body any, response any) error {
var bodyBytes []byte
var err error
if body != nil {
if !checkByteSlice(body) {
bodyBytes, err = json.Marshal(body)
if err != nil {
return fmt.Errorf("failed to marshal request body: %w", err)
}
contentType = "application/json"
} else {
bodyBytes = body.([]byte)
}
}
if len(bodyBytes) == 0 {
bodyBytes = []byte("")
}
fullURL := c.baseURL + path
req, err := http.NewRequestWithContext(ctx, method, fullURL, bytes.NewBuffer(bodyBytes))
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}
timestamp := fmt.Sprintf("%d", time.Now().Unix())
authHeader := c.generateAuthHeader(method, path, timestamp, contentType, bodyBytes)
req.Header.Set("Authorization", authHeader)
req.Header.Set("Dragonchain", c.publicID)
req.Header.Set("Timestamp", timestamp)
if contentType != "" {
req.Header.Set("Content-Type", contentType)
}
resp, err := c.httpClient.Do(req)
if err != nil {
return fmt.Errorf("failed to execute request: %w", err)
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("failed to read response body: %w", err)
}
if resp.StatusCode >= 400 {
return fmt.Errorf("API error (status %d): %s", resp.StatusCode, strings.TrimSpace(string(respBody)))
}
if response != nil && len(respBody) > 0 {
if err := json.Unmarshal(respBody, response); err != nil {
return fmt.Errorf("failed to unmarshal response: %w", err)
}
}
return nil
}
func checkByteSlice(body interface{}) bool {
_, ok := body.([]byte)
return ok
}
func (c *Client) Get(ctx context.Context, path string, response any) error {
return c.doRequest(ctx, http.MethodGet, path, "", nil, response)
}
func (c *Client) Post(ctx context.Context, path, contentType string, body any, response any) error {
return c.doRequest(ctx, http.MethodPost, path, contentType, body, response)
}
func (c *Client) Put(ctx context.Context, path, contentType string, body any, response any) error {
return c.doRequest(ctx, http.MethodPut, path, contentType, body, response)
}
func (c *Client) Delete(ctx context.Context, path string, response any) error {
return c.doRequest(ctx, http.MethodDelete, path, "", nil, response)
}