package client import ( "bytes" "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(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.NewRequest(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(path string, response any) error { return c.doRequest(http.MethodGet, path, "", nil, response) } func (c *Client) Post(path, contentType string, body any, response any) error { return c.doRequest(http.MethodPost, path, contentType, body, response) } func (c *Client) Put(path, contentType string, body any, response any) error { return c.doRequest(http.MethodPut, path, contentType, body, response) } func (c *Client) Delete(path string, response any) error { return c.doRequest(http.MethodDelete, path, "", nil, response) }