Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions internal/adapters/apis/beaconchain/beaconchain_adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package beaconchain

import (
"bytes"
"clients-test/internal/logger"
"context"
"encoding/json"
"fmt"
Expand All @@ -14,29 +15,35 @@ import (
type BeaconchainAdapter struct {
beaconChainUrl string
client *http.Client
logPrefix string
}

// NewBeaconchainAdapter creates a new BeaconchainAdapter
func NewBeaconchainAdapter(beaconChainUrl string) *BeaconchainAdapter {
return &BeaconchainAdapter{
beaconChainUrl: beaconChainUrl,
client: &http.Client{},
logPrefix: "BeaconchainAdapter",
}
}

// GetIsSyncing retrieves the syncing status from the beacon node with context
func (b *BeaconchainAdapter) GetIsSyncing(ctx context.Context) (bool, error) {
url := fmt.Sprintf("%s/eth/v1/node/syncing", b.beaconChainUrl)
logger.Debug("[BeaconchainAdapter] GetIsSyncing: url=%s", url)
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
logger.ErrorWithPrefix(b.logPrefix, "GetIsSyncing: failed to create request: %v", err)
return false, err
}
resp, err := b.client.Do(req)
if err != nil {
logger.ErrorWithPrefix(b.logPrefix, "GetIsSyncing: request failed: %v", err)
return false, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
logger.ErrorWithPrefix(b.logPrefix, "GetIsSyncing: non-200 status: %s", resp.Status)
return false, fmt.Errorf("beacon node syncing failed: %s", resp.Status)
}
var result struct {
Expand All @@ -45,8 +52,10 @@ func (b *BeaconchainAdapter) GetIsSyncing(ctx context.Context) (bool, error) {
} `json:"data"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
logger.ErrorWithPrefix(b.logPrefix, "GetIsSyncing: failed to decode response: %v", err)
return false, err
}
logger.DebugWithPrefix(b.logPrefix, "GetIsSyncing: result=%+v", result.Data.IsSyncing)
return result.Data.IsSyncing, nil
}

Expand All @@ -64,29 +73,36 @@ type blockHeaderResponse struct {
// getBlockHeader retrieves the block header for a given block ID
func (b *BeaconchainAdapter) getBlockHeader(ctx context.Context, blockID string) (*blockHeaderResponse, error) {
url := fmt.Sprintf("%s/eth/v1/beacon/headers/%s", b.beaconChainUrl, blockID)
logger.Debug("[BeaconchainAdapter] getBlockHeader: url=%s", url)
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
logger.ErrorWithPrefix(b.logPrefix, "getBlockHeader: failed to create request: %v", err)
return nil, fmt.Errorf("failed to send request to Beaconchain at %s: %w", url, err)
}
resp, err := b.client.Do(req)
if err != nil {
logger.ErrorWithPrefix(b.logPrefix, "getBlockHeader: request failed: %v", err)
return nil, err
}
defer resp.Body.Close()
var result blockHeaderResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
logger.ErrorWithPrefix(b.logPrefix, "getBlockHeader: failed to decode response: %v", err)
return nil, fmt.Errorf("failed to decode response for GetBlockHeader: %w", err)
}
logger.DebugWithPrefix(b.logPrefix, "getBlockHeader: slot=%s", result.Data.Header.Message.Slot)
return &result, nil
}

func (b *BeaconchainAdapter) getEpochFinalized(ctx context.Context, blockID string) (uint64, error) {
header, err := b.getBlockHeader(ctx, blockID)
if err != nil {
logger.ErrorWithPrefix(b.logPrefix, "getEpochFinalized: failed to get block header for blockID %s: %v", blockID, err)
return 0, fmt.Errorf("failed to get block header for blockID %s: %w", blockID, err)
}
slot := header.Data.Header.Message.Slot
epoch := getEpochFromSlot(slot)
logger.DebugWithPrefix(b.logPrefix, "getEpochFinalized: slot=%s epoch=%d", slot, epoch)
return epoch, nil
}

Expand All @@ -106,6 +122,7 @@ func parseInt(slot string) uint64 {
func (b *BeaconchainAdapter) GetValidatorLiveness(ctx context.Context, indexes []string) (map[string]bool, error) {
epoch, err := b.getEpochFinalized(ctx, "finalized")
if err != nil {
logger.ErrorWithPrefix(b.logPrefix, "GetValidatorLiveness: failed to get epoch finalized: %v", err)
return nil, err
}
// Join indexes as comma-separated string
Expand All @@ -117,16 +134,20 @@ func (b *BeaconchainAdapter) GetValidatorLiveness(ctx context.Context, indexes [
joined += idx
}
url := fmt.Sprintf("%s/eth/v1/validator/liveness/%d?indices=%s", b.beaconChainUrl, epoch, joined)
logger.DebugWithPrefix(b.logPrefix, "GetValidatorLiveness: url=%s", url)
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
logger.ErrorWithPrefix(b.logPrefix, "GetValidatorLiveness: failed to create request: %v", err)
return nil, err
}
resp, err := b.client.Do(req)
if err != nil {
logger.ErrorWithPrefix(b.logPrefix, "GetValidatorLiveness: request failed: %v", err)
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
logger.ErrorWithPrefix(b.logPrefix, "GetValidatorLiveness: non-200 status: %s", resp.Status)
return nil, fmt.Errorf("validator liveness failed: %s", resp.Status)
}
var result struct {
Expand All @@ -136,18 +157,21 @@ func (b *BeaconchainAdapter) GetValidatorLiveness(ctx context.Context, indexes [
} `json:"data"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
logger.ErrorWithPrefix(b.logPrefix, "GetValidatorLiveness: failed to decode response: %v", err)
return nil, err
}
liveness := make(map[string]bool)
for _, v := range result.Data {
liveness[v.Index] = v.IsLive
}
logger.DebugWithPrefix(b.logPrefix, "GetValidatorLiveness: liveness=%+v", liveness)
return liveness, nil
}

// GetValidatorsIndexes retrieves the validator index for each given pubkey with status active_ongoing
func (b *BeaconchainAdapter) GetValidatorsIndexes(ctx context.Context, pubkeys []string) ([]string, error) {
url := fmt.Sprintf("%s/eth/v1/beacon/states/finalized/validators", b.beaconChainUrl)
logger.DebugWithPrefix(b.logPrefix, "GetValidatorsIndexes: url=%s pubkeys=%+v", url, pubkeys)
requestBody := struct {
IDs []string `json:"ids"`
Statuses []string `json:"statuses"`
Expand All @@ -157,22 +181,26 @@ func (b *BeaconchainAdapter) GetValidatorsIndexes(ctx context.Context, pubkeys [
}
jsonBytes, err := json.Marshal(requestBody)
if err != nil {
logger.ErrorWithPrefix(b.logPrefix, "GetValidatorsIndexes: failed to marshal request body: %v", err)
return nil, err
}

req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(jsonBytes))
if err != nil {
logger.ErrorWithPrefix(b.logPrefix, "GetValidatorsIndexes: failed to create request: %v", err)
return nil, err
}
req.Header.Set("Content-Type", "application/json")

resp, err := b.client.Do(req)
if err != nil {
logger.ErrorWithPrefix(b.logPrefix, "GetValidatorsIndexes: request failed: %v", err)
return nil, err
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
logger.ErrorWithPrefix(b.logPrefix, "GetValidatorsIndexes: non-200 status: %s", resp.Status)
return nil, fmt.Errorf("get validators indexes failed: %s", resp.Status)
}

Expand All @@ -185,11 +213,13 @@ func (b *BeaconchainAdapter) GetValidatorsIndexes(ctx context.Context, pubkeys [
} `json:"data"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
logger.ErrorWithPrefix(b.logPrefix, "GetValidatorsIndexes: failed to decode response: %v", err)
return nil, err
}
indexes := make([]string, len(result.Data))
for i, v := range result.Data {
indexes[i] = v.Index
}
logger.DebugWithPrefix(b.logPrefix, "GetValidatorsIndexes: indexes=%+v", indexes)
return indexes, nil
}
18 changes: 14 additions & 4 deletions internal/adapters/apis/brain/brain_adapter.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package brain

import (
"clients-test/internal/logger"
"context"
"encoding/json"
"fmt"
Expand All @@ -11,42 +12,51 @@ import (
// It interacts with the brain service to fetch validator indexes
// Example endpoint: /v0/brain/validators?tag=solo&format=index
type BrainAdapter struct {
brainUrl string
client *http.Client
brainUrl string
client *http.Client
logPrefix string
}

// NewBrainAdapter creates a new BrainAdapter
func NewBrainAdapter(brainUrl string) *BrainAdapter {
return &BrainAdapter{
brainUrl: brainUrl,
client: &http.Client{},
brainUrl: brainUrl,
client: &http.Client{},
logPrefix: "BrainAdapter",
}
}

// GetValidatorsPubkeys fetches the validator public keys from the brain service with context
func (b *BrainAdapter) GetValidatorsPubkeys(ctx context.Context) ([]string, error) {
url := fmt.Sprintf("%s/v0/brain/validators?tag=solo&format=pubkey", b.brainUrl)
logger.DebugWithPrefix(b.logPrefix, "GetValidatorsPubkeys: url=%s", url)
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
logger.ErrorWithPrefix(b.logPrefix, "GetValidatorsPubkeys: failed to create request: %v", err)
return nil, err
}
resp, err := b.client.Do(req)
if err != nil {
logger.ErrorWithPrefix(b.logPrefix, "GetValidatorsPubkeys: request failed: %v", err)
return nil, err
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
logger.ErrorWithPrefix(b.logPrefix, "GetValidatorsPubkeys: non-200 status: %s", resp.Status)
return nil, fmt.Errorf("brain service error: %s", resp.Status)
}

var tagValidators map[string][]string
if err := json.NewDecoder(resp.Body).Decode(&tagValidators); err != nil {
logger.ErrorWithPrefix(b.logPrefix, "GetValidatorsPubkeys: failed to decode response: %v", err)
return nil, err
}
pubkeys, ok := tagValidators["solo"]
if !ok {
logger.ErrorWithPrefix(b.logPrefix, "GetValidatorsPubkeys: no 'solo' tag found in response")
return nil, fmt.Errorf("no 'solo' tag found in response")
}
logger.DebugWithPrefix(b.logPrefix, "GetValidatorsPubkeys: pubkeys=%+v", pubkeys)
return pubkeys, nil
}
Loading