Privacy SDK features: - Confidential transactions with Pedersen commitments - Bulletproof range proofs for value validation - Ring signatures for anonymous signing with key images - Stealth addresses for unlinkable payments - Blinding factor generation and value operations Contract SDK features: - Smart contract deployment (standard and CREATE2) - Call (view/pure) and Send (state-changing) operations - Event log filtering, subscription, and decoding - ABI encoding/decoding utilities - Gas estimation and contract verification - Multicall for batched operations - Storage slot reading Languages implemented: - JavaScript/TypeScript - Python (async with httpx) - Go - Rust (async with reqwest/tokio) - Java (async with OkHttp) - Kotlin (coroutines with Ktor) - Swift (async/await with URLSession) - Flutter/Dart - C (header-only interface) - C++ (header-only with std::future) - C#/.NET (async with HttpClient) - Ruby (Faraday HTTP client) All SDKs follow consistent patterns: - Configuration with API key, endpoint, timeout, retries - Custom exception types with error codes - Retry logic with exponential backoff - Health check endpoints - Closed state management
682 lines
19 KiB
Go
682 lines
19 KiB
Go
// Package privacy provides privacy-enhancing cryptographic features.
|
|
package privacy
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"sync/atomic"
|
|
"time"
|
|
)
|
|
|
|
const DefaultEndpoint = "https://privacy.synor.cc/api/v1"
|
|
|
|
// Config is the privacy client configuration.
|
|
type Config struct {
|
|
APIKey string
|
|
Endpoint string
|
|
Timeout time.Duration
|
|
Retries int
|
|
Debug bool
|
|
}
|
|
|
|
// KeyType represents the type of cryptographic key.
|
|
type KeyType string
|
|
|
|
const (
|
|
KeyTypeEd25519 KeyType = "ed25519"
|
|
KeyTypeSecp256k1 KeyType = "secp256k1"
|
|
)
|
|
|
|
// GeneratorType represents the generator used for commitments.
|
|
type GeneratorType string
|
|
|
|
const (
|
|
GeneratorDefault GeneratorType = "default"
|
|
GeneratorAlternate GeneratorType = "alternate"
|
|
)
|
|
|
|
// PublicKey represents a public key.
|
|
type PublicKey struct {
|
|
Key string `json:"key"`
|
|
Type KeyType `json:"type"`
|
|
}
|
|
|
|
// PrivateKey represents a private key.
|
|
type PrivateKey struct {
|
|
Key string `json:"key"`
|
|
Type KeyType `json:"type"`
|
|
}
|
|
|
|
// ConfidentialUTXO is a UTXO for confidential transactions.
|
|
type ConfidentialUTXO struct {
|
|
TxID string `json:"txid"`
|
|
Vout int `json:"vout"`
|
|
Commitment string `json:"commitment"`
|
|
RangeProof string `json:"rangeProof"`
|
|
Blinding string `json:"blinding,omitempty"`
|
|
Amount string `json:"amount,omitempty"`
|
|
}
|
|
|
|
// ConfidentialOutput is an output for confidential transactions.
|
|
type ConfidentialOutput struct {
|
|
Recipient string `json:"recipient"`
|
|
Amount string `json:"amount"`
|
|
Blinding string `json:"blinding,omitempty"`
|
|
}
|
|
|
|
// OutputFeatures contains output feature flags.
|
|
type OutputFeatures struct {
|
|
Flags int `json:"flags"`
|
|
LockHeight int `json:"lockHeight,omitempty"`
|
|
}
|
|
|
|
// ConfidentialTxInput is a confidential transaction input.
|
|
type ConfidentialTxInput struct {
|
|
OutputRef string `json:"outputRef"`
|
|
Commitment string `json:"commitment"`
|
|
}
|
|
|
|
// ConfidentialTxOutput is a confidential transaction output.
|
|
type ConfidentialTxOutput struct {
|
|
Commitment string `json:"commitment"`
|
|
RangeProof string `json:"rangeProof"`
|
|
Features OutputFeatures `json:"features"`
|
|
}
|
|
|
|
// TransactionKernel is the kernel for confidential transactions.
|
|
type TransactionKernel struct {
|
|
Features int `json:"features"`
|
|
Fee string `json:"fee"`
|
|
LockHeight int `json:"lockHeight"`
|
|
Excess string `json:"excess"`
|
|
ExcessSignature string `json:"excessSignature"`
|
|
}
|
|
|
|
// ConfidentialTransaction is a confidential transaction.
|
|
type ConfidentialTransaction struct {
|
|
TxID string `json:"txid"`
|
|
Version int `json:"version"`
|
|
Inputs []ConfidentialTxInput `json:"inputs"`
|
|
Outputs []ConfidentialTxOutput `json:"outputs"`
|
|
Kernel TransactionKernel `json:"kernel"`
|
|
Offset string `json:"offset"`
|
|
Raw string `json:"raw"`
|
|
}
|
|
|
|
// VerifyConfidentialTxDetails contains verification details.
|
|
type VerifyConfidentialTxDetails struct {
|
|
CommitmentsBalance bool `json:"commitmentsBalance"`
|
|
RangeProofsValid bool `json:"rangeProofsValid"`
|
|
SignatureValid bool `json:"signatureValid"`
|
|
NoDuplicateInputs bool `json:"noDuplicateInputs"`
|
|
}
|
|
|
|
// VerifyConfidentialTxResult is the result of verifying a confidential transaction.
|
|
type VerifyConfidentialTxResult struct {
|
|
Valid bool `json:"valid"`
|
|
Details VerifyConfidentialTxDetails `json:"details"`
|
|
Error string `json:"error,omitempty"`
|
|
}
|
|
|
|
// RingSignatureComponents contains the signature components.
|
|
type RingSignatureComponents struct {
|
|
C []string `json:"c"`
|
|
R []string `json:"r"`
|
|
}
|
|
|
|
// RingSignature is a ring signature.
|
|
type RingSignature struct {
|
|
ID string `json:"id"`
|
|
MessageHash string `json:"messageHash"`
|
|
Ring []string `json:"ring"`
|
|
KeyImage string `json:"keyImage"`
|
|
Signature RingSignatureComponents `json:"signature"`
|
|
RingSize int `json:"ringSize"`
|
|
}
|
|
|
|
// VerifyRingSignatureResult is the result of verifying a ring signature.
|
|
type VerifyRingSignatureResult struct {
|
|
Valid bool `json:"valid"`
|
|
KeyImage string `json:"keyImage"`
|
|
}
|
|
|
|
// StealthAddress is a stealth address.
|
|
type StealthAddress struct {
|
|
Address string `json:"address"`
|
|
ScanPublicKey string `json:"scanPublicKey"`
|
|
SpendPublicKey string `json:"spendPublicKey"`
|
|
}
|
|
|
|
// StealthKeypair is a stealth address keypair.
|
|
type StealthKeypair struct {
|
|
Address StealthAddress `json:"address"`
|
|
ScanPrivateKey string `json:"scanPrivateKey"`
|
|
SpendPrivateKey string `json:"spendPrivateKey"`
|
|
}
|
|
|
|
// OneTimeAddress is a one-time address derived from a stealth address.
|
|
type OneTimeAddress struct {
|
|
Address string `json:"address"`
|
|
EphemeralPublicKey string `json:"ephemeralPublicKey"`
|
|
SharedSecret string `json:"sharedSecret,omitempty"`
|
|
}
|
|
|
|
// SharedSecret is the result of deriving a shared secret.
|
|
type SharedSecret struct {
|
|
Secret string `json:"secret"`
|
|
OneTimePrivateKey string `json:"oneTimePrivateKey"`
|
|
OneTimeAddress string `json:"oneTimeAddress"`
|
|
}
|
|
|
|
// Commitment is a Pedersen commitment.
|
|
type Commitment struct {
|
|
Commitment string `json:"commitment"`
|
|
Generator GeneratorType `json:"generator"`
|
|
}
|
|
|
|
// CommitmentWithBlinding is a commitment with its blinding factor.
|
|
type CommitmentWithBlinding struct {
|
|
Commitment Commitment `json:"commitment"`
|
|
Blinding string `json:"blinding"`
|
|
}
|
|
|
|
// RangeProof is a Bulletproof range proof.
|
|
type RangeProof struct {
|
|
Proof string `json:"proof"`
|
|
Commitment string `json:"commitment"`
|
|
BitLength int `json:"bitLength"`
|
|
}
|
|
|
|
// VerifyRangeProofResult is the result of verifying a range proof.
|
|
type VerifyRangeProofResult struct {
|
|
Valid bool `json:"valid"`
|
|
MinValue string `json:"minValue"`
|
|
MaxValue string `json:"maxValue"`
|
|
}
|
|
|
|
// Error is a privacy error.
|
|
type Error struct {
|
|
Message string
|
|
Code string
|
|
StatusCode int
|
|
}
|
|
|
|
func (e *Error) Error() string {
|
|
return e.Message
|
|
}
|
|
|
|
// Client is a Synor Privacy client.
|
|
type Client struct {
|
|
config Config
|
|
httpClient *http.Client
|
|
closed atomic.Bool
|
|
}
|
|
|
|
// NewClient creates a new privacy client.
|
|
func NewClient(config Config) *Client {
|
|
if config.Endpoint == "" {
|
|
config.Endpoint = DefaultEndpoint
|
|
}
|
|
if config.Timeout == 0 {
|
|
config.Timeout = 30 * time.Second
|
|
}
|
|
if config.Retries == 0 {
|
|
config.Retries = 3
|
|
}
|
|
|
|
return &Client{
|
|
config: config,
|
|
httpClient: &http.Client{
|
|
Timeout: config.Timeout,
|
|
},
|
|
}
|
|
}
|
|
|
|
// ==================== Confidential Transactions ====================
|
|
|
|
// CreateConfidentialTxOptions are options for creating a confidential transaction.
|
|
type CreateConfidentialTxOptions struct {
|
|
Inputs []ConfidentialUTXO
|
|
Outputs []ConfidentialOutput
|
|
Fee string
|
|
LockHeight int
|
|
}
|
|
|
|
// CreateConfidentialTransaction creates a confidential transaction.
|
|
func (c *Client) CreateConfidentialTransaction(ctx context.Context, opts CreateConfidentialTxOptions) (*ConfidentialTransaction, error) {
|
|
body := map[string]interface{}{
|
|
"inputs": opts.Inputs,
|
|
"outputs": opts.Outputs,
|
|
}
|
|
if opts.Fee != "" {
|
|
body["fee"] = opts.Fee
|
|
}
|
|
if opts.LockHeight > 0 {
|
|
body["lockHeight"] = opts.LockHeight
|
|
}
|
|
|
|
var resp struct {
|
|
Transaction ConfidentialTransaction `json:"transaction"`
|
|
}
|
|
if err := c.request(ctx, "POST", "/transactions/confidential", body, &resp); err != nil {
|
|
return nil, err
|
|
}
|
|
return &resp.Transaction, nil
|
|
}
|
|
|
|
// VerifyConfidentialTransaction verifies a confidential transaction.
|
|
func (c *Client) VerifyConfidentialTransaction(ctx context.Context, tx *ConfidentialTransaction) (*VerifyConfidentialTxResult, error) {
|
|
body := map[string]interface{}{
|
|
"transaction": tx.Raw,
|
|
}
|
|
var result VerifyConfidentialTxResult
|
|
if err := c.request(ctx, "POST", "/transactions/confidential/verify", body, &result); err != nil {
|
|
return nil, err
|
|
}
|
|
return &result, nil
|
|
}
|
|
|
|
// DecodeConfidentialOutput decodes a confidential output.
|
|
func (c *Client) DecodeConfidentialOutput(ctx context.Context, commitment, blinding string) (string, error) {
|
|
body := map[string]interface{}{
|
|
"commitment": commitment,
|
|
"blinding": blinding,
|
|
}
|
|
var resp struct {
|
|
Amount string `json:"amount"`
|
|
}
|
|
if err := c.request(ctx, "POST", "/transactions/confidential/decode", body, &resp); err != nil {
|
|
return "", err
|
|
}
|
|
return resp.Amount, nil
|
|
}
|
|
|
|
// ==================== Ring Signatures ====================
|
|
|
|
// CreateRingSignatureOptions are options for creating a ring signature.
|
|
type CreateRingSignatureOptions struct {
|
|
Message string
|
|
Ring []string
|
|
PrivateKey string
|
|
SignerIndex *int
|
|
}
|
|
|
|
// CreateRingSignature creates a ring signature.
|
|
func (c *Client) CreateRingSignature(ctx context.Context, opts CreateRingSignatureOptions) (*RingSignature, error) {
|
|
body := map[string]interface{}{
|
|
"message": opts.Message,
|
|
"ring": opts.Ring,
|
|
"privateKey": opts.PrivateKey,
|
|
}
|
|
if opts.SignerIndex != nil {
|
|
body["signerIndex"] = *opts.SignerIndex
|
|
}
|
|
|
|
var resp struct {
|
|
Signature RingSignature `json:"signature"`
|
|
}
|
|
if err := c.request(ctx, "POST", "/ring-signatures/create", body, &resp); err != nil {
|
|
return nil, err
|
|
}
|
|
return &resp.Signature, nil
|
|
}
|
|
|
|
// VerifyRingSignature verifies a ring signature.
|
|
func (c *Client) VerifyRingSignature(ctx context.Context, signature *RingSignature, message string) (*VerifyRingSignatureResult, error) {
|
|
body := map[string]interface{}{
|
|
"signature": signature,
|
|
"message": message,
|
|
}
|
|
var result VerifyRingSignatureResult
|
|
if err := c.request(ctx, "POST", "/ring-signatures/verify", body, &result); err != nil {
|
|
return nil, err
|
|
}
|
|
return &result, nil
|
|
}
|
|
|
|
// IsKeyImageUsed checks if a key image has been used.
|
|
func (c *Client) IsKeyImageUsed(ctx context.Context, keyImage string) (bool, error) {
|
|
var resp struct {
|
|
Used bool `json:"used"`
|
|
}
|
|
if err := c.request(ctx, "GET", fmt.Sprintf("/ring-signatures/key-images/%s", keyImage), nil, &resp); err != nil {
|
|
return false, err
|
|
}
|
|
return resp.Used, nil
|
|
}
|
|
|
|
// GenerateRandomRing generates a random ring of public keys.
|
|
func (c *Client) GenerateRandomRing(ctx context.Context, size int, exclude []string) ([]string, error) {
|
|
body := map[string]interface{}{
|
|
"size": size,
|
|
}
|
|
if len(exclude) > 0 {
|
|
body["exclude"] = exclude
|
|
}
|
|
var resp struct {
|
|
Ring []string `json:"ring"`
|
|
}
|
|
if err := c.request(ctx, "POST", "/ring-signatures/random-ring", body, &resp); err != nil {
|
|
return nil, err
|
|
}
|
|
return resp.Ring, nil
|
|
}
|
|
|
|
// ==================== Stealth Addresses ====================
|
|
|
|
// GenerateStealthKeypair generates a new stealth address keypair.
|
|
func (c *Client) GenerateStealthKeypair(ctx context.Context) (*StealthKeypair, error) {
|
|
var resp struct {
|
|
Keypair StealthKeypair `json:"keypair"`
|
|
}
|
|
if err := c.request(ctx, "POST", "/stealth/generate", nil, &resp); err != nil {
|
|
return nil, err
|
|
}
|
|
return &resp.Keypair, nil
|
|
}
|
|
|
|
// CreateOneTimeAddress creates a one-time address for a stealth address.
|
|
func (c *Client) CreateOneTimeAddress(ctx context.Context, stealthAddress *StealthAddress) (*OneTimeAddress, error) {
|
|
body := map[string]interface{}{
|
|
"scanPublicKey": stealthAddress.ScanPublicKey,
|
|
"spendPublicKey": stealthAddress.SpendPublicKey,
|
|
}
|
|
var resp struct {
|
|
OneTimeAddress OneTimeAddress `json:"oneTimeAddress"`
|
|
}
|
|
if err := c.request(ctx, "POST", "/stealth/one-time-address", body, &resp); err != nil {
|
|
return nil, err
|
|
}
|
|
return &resp.OneTimeAddress, nil
|
|
}
|
|
|
|
// DeriveSharedSecret derives the shared secret for a stealth payment.
|
|
func (c *Client) DeriveSharedSecret(ctx context.Context, stealthAddress *StealthAddress, privateKey, ephemeralPublicKey string) (*SharedSecret, error) {
|
|
body := map[string]interface{}{
|
|
"scanPublicKey": stealthAddress.ScanPublicKey,
|
|
"spendPublicKey": stealthAddress.SpendPublicKey,
|
|
"privateKey": privateKey,
|
|
"ephemeralPublicKey": ephemeralPublicKey,
|
|
}
|
|
var secret SharedSecret
|
|
if err := c.request(ctx, "POST", "/stealth/derive-secret", body, &secret); err != nil {
|
|
return nil, err
|
|
}
|
|
return &secret, nil
|
|
}
|
|
|
|
// ScanForPayments scans transactions for stealth payments.
|
|
func (c *Client) ScanForPayments(ctx context.Context, scanPrivateKey, spendPublicKey string, transactions []string) ([]OneTimeAddress, error) {
|
|
body := map[string]interface{}{
|
|
"scanPrivateKey": scanPrivateKey,
|
|
"spendPublicKey": spendPublicKey,
|
|
"transactions": transactions,
|
|
}
|
|
var resp struct {
|
|
Payments []OneTimeAddress `json:"payments"`
|
|
}
|
|
if err := c.request(ctx, "POST", "/stealth/scan", body, &resp); err != nil {
|
|
return nil, err
|
|
}
|
|
return resp.Payments, nil
|
|
}
|
|
|
|
// ==================== Commitments ====================
|
|
|
|
// CreateCommitment creates a Pedersen commitment.
|
|
func (c *Client) CreateCommitment(ctx context.Context, value string, blinding string) (*CommitmentWithBlinding, error) {
|
|
body := map[string]interface{}{
|
|
"value": value,
|
|
}
|
|
if blinding != "" {
|
|
body["blinding"] = blinding
|
|
}
|
|
var result CommitmentWithBlinding
|
|
if err := c.request(ctx, "POST", "/commitments/create", body, &result); err != nil {
|
|
return nil, err
|
|
}
|
|
return &result, nil
|
|
}
|
|
|
|
// OpenCommitment verifies a Pedersen commitment.
|
|
func (c *Client) OpenCommitment(ctx context.Context, commitment, value, blinding string) (bool, error) {
|
|
body := map[string]interface{}{
|
|
"commitment": commitment,
|
|
"value": value,
|
|
"blinding": blinding,
|
|
}
|
|
var resp struct {
|
|
Valid bool `json:"valid"`
|
|
}
|
|
if err := c.request(ctx, "POST", "/commitments/open", body, &resp); err != nil {
|
|
return false, err
|
|
}
|
|
return resp.Valid, nil
|
|
}
|
|
|
|
// AddCommitments adds two commitments.
|
|
func (c *Client) AddCommitments(ctx context.Context, commitment1, commitment2 string) (string, error) {
|
|
body := map[string]interface{}{
|
|
"commitment1": commitment1,
|
|
"commitment2": commitment2,
|
|
}
|
|
var resp struct {
|
|
Commitment string `json:"commitment"`
|
|
}
|
|
if err := c.request(ctx, "POST", "/commitments/add", body, &resp); err != nil {
|
|
return "", err
|
|
}
|
|
return resp.Commitment, nil
|
|
}
|
|
|
|
// SubtractCommitments subtracts two commitments.
|
|
func (c *Client) SubtractCommitments(ctx context.Context, commitment1, commitment2 string) (string, error) {
|
|
body := map[string]interface{}{
|
|
"commitment1": commitment1,
|
|
"commitment2": commitment2,
|
|
}
|
|
var resp struct {
|
|
Commitment string `json:"commitment"`
|
|
}
|
|
if err := c.request(ctx, "POST", "/commitments/subtract", body, &resp); err != nil {
|
|
return "", err
|
|
}
|
|
return resp.Commitment, nil
|
|
}
|
|
|
|
// ComputeBlindingSum computes the sum of blinding factors.
|
|
func (c *Client) ComputeBlindingSum(ctx context.Context, positive, negative []string) (string, error) {
|
|
body := map[string]interface{}{
|
|
"positive": positive,
|
|
"negative": negative,
|
|
}
|
|
var resp struct {
|
|
Sum string `json:"sum"`
|
|
}
|
|
if err := c.request(ctx, "POST", "/commitments/blinding-sum", body, &resp); err != nil {
|
|
return "", err
|
|
}
|
|
return resp.Sum, nil
|
|
}
|
|
|
|
// GenerateBlinding generates a random blinding factor.
|
|
func (c *Client) GenerateBlinding(ctx context.Context) (string, error) {
|
|
var resp struct {
|
|
Blinding string `json:"blinding"`
|
|
}
|
|
if err := c.request(ctx, "POST", "/commitments/random-blinding", nil, &resp); err != nil {
|
|
return "", err
|
|
}
|
|
return resp.Blinding, nil
|
|
}
|
|
|
|
// ==================== Range Proofs ====================
|
|
|
|
// CreateRangeProofOptions are options for creating a range proof.
|
|
type CreateRangeProofOptions struct {
|
|
Value string
|
|
Blinding string
|
|
Message string
|
|
BitLength int
|
|
}
|
|
|
|
// CreateRangeProof creates a Bulletproof range proof.
|
|
func (c *Client) CreateRangeProof(ctx context.Context, opts CreateRangeProofOptions) (*RangeProof, error) {
|
|
body := map[string]interface{}{
|
|
"value": opts.Value,
|
|
"blinding": opts.Blinding,
|
|
}
|
|
if opts.Message != "" {
|
|
body["message"] = opts.Message
|
|
}
|
|
if opts.BitLength > 0 {
|
|
body["bitLength"] = opts.BitLength
|
|
} else {
|
|
body["bitLength"] = 64
|
|
}
|
|
|
|
var resp struct {
|
|
Proof RangeProof `json:"proof"`
|
|
}
|
|
if err := c.request(ctx, "POST", "/range-proofs/create", body, &resp); err != nil {
|
|
return nil, err
|
|
}
|
|
return &resp.Proof, nil
|
|
}
|
|
|
|
// VerifyRangeProof verifies a Bulletproof range proof.
|
|
func (c *Client) VerifyRangeProof(ctx context.Context, commitment, proof string) (*VerifyRangeProofResult, error) {
|
|
body := map[string]interface{}{
|
|
"commitment": commitment,
|
|
"proof": proof,
|
|
}
|
|
var result VerifyRangeProofResult
|
|
if err := c.request(ctx, "POST", "/range-proofs/verify", body, &result); err != nil {
|
|
return nil, err
|
|
}
|
|
return &result, nil
|
|
}
|
|
|
|
// CreateAggregatedRangeProof creates an aggregated range proof.
|
|
func (c *Client) CreateAggregatedRangeProof(ctx context.Context, outputs []map[string]string) (string, error) {
|
|
body := map[string]interface{}{
|
|
"outputs": outputs,
|
|
}
|
|
var resp struct {
|
|
Proof string `json:"proof"`
|
|
}
|
|
if err := c.request(ctx, "POST", "/range-proofs/aggregate", body, &resp); err != nil {
|
|
return "", err
|
|
}
|
|
return resp.Proof, nil
|
|
}
|
|
|
|
// ==================== Lifecycle ====================
|
|
|
|
// HealthCheck checks if the service is healthy.
|
|
func (c *Client) HealthCheck(ctx context.Context) bool {
|
|
var resp struct {
|
|
Status string `json:"status"`
|
|
}
|
|
if err := c.request(ctx, "GET", "/health", nil, &resp); err != nil {
|
|
return false
|
|
}
|
|
return resp.Status == "healthy"
|
|
}
|
|
|
|
// Close closes the client.
|
|
func (c *Client) Close() {
|
|
c.closed.Store(true)
|
|
}
|
|
|
|
// IsClosed returns whether the client is closed.
|
|
func (c *Client) IsClosed() bool {
|
|
return c.closed.Load()
|
|
}
|
|
|
|
// ==================== Private ====================
|
|
|
|
func (c *Client) request(ctx context.Context, method, path string, body interface{}, result interface{}) error {
|
|
if c.closed.Load() {
|
|
return &Error{Message: "client has been closed"}
|
|
}
|
|
|
|
var lastErr error
|
|
for attempt := 0; attempt < c.config.Retries; attempt++ {
|
|
err := c.doRequest(ctx, method, path, body, result)
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
lastErr = err
|
|
if attempt < c.config.Retries-1 {
|
|
time.Sleep(time.Duration(1<<attempt) * time.Second)
|
|
}
|
|
}
|
|
return lastErr
|
|
}
|
|
|
|
func (c *Client) doRequest(ctx context.Context, method, path string, body interface{}, result interface{}) error {
|
|
url := c.config.Endpoint + path
|
|
|
|
var bodyReader io.Reader
|
|
if body != nil {
|
|
jsonBody, err := json.Marshal(body)
|
|
if err != nil {
|
|
return &Error{Message: fmt.Sprintf("failed to marshal body: %v", err)}
|
|
}
|
|
bodyReader = bytes.NewReader(jsonBody)
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, method, url, bodyReader)
|
|
if err != nil {
|
|
return &Error{Message: fmt.Sprintf("failed to create request: %v", err)}
|
|
}
|
|
|
|
req.Header.Set("Authorization", "Bearer "+c.config.APIKey)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("X-SDK-Version", "go/0.1.0")
|
|
|
|
resp, err := c.httpClient.Do(req)
|
|
if err != nil {
|
|
return &Error{Message: fmt.Sprintf("request failed: %v", err)}
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
respBody, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return &Error{Message: fmt.Sprintf("failed to read response: %v", err)}
|
|
}
|
|
|
|
if resp.StatusCode >= 400 {
|
|
var errResp struct {
|
|
Message string `json:"message"`
|
|
Error string `json:"error"`
|
|
Code string `json:"code"`
|
|
}
|
|
json.Unmarshal(respBody, &errResp)
|
|
msg := errResp.Message
|
|
if msg == "" {
|
|
msg = errResp.Error
|
|
}
|
|
if msg == "" {
|
|
msg = fmt.Sprintf("HTTP %d", resp.StatusCode)
|
|
}
|
|
return &Error{
|
|
Message: msg,
|
|
Code: errResp.Code,
|
|
StatusCode: resp.StatusCode,
|
|
}
|
|
}
|
|
|
|
if result != nil {
|
|
if err := json.Unmarshal(respBody, result); err != nil {
|
|
return &Error{Message: fmt.Sprintf("failed to parse response: %v", err)}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|