commit inicial do projeto
This commit is contained in:
59
internal/oracle/fetcher.go
Normal file
59
internal/oracle/fetcher.go
Normal file
@ -0,0 +1,59 @@
|
||||
package oracle
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// OracleData representa um dado externo coletado via oráculo
|
||||
type OracleData struct {
|
||||
Source string `json:"source"`
|
||||
Key string `json:"key"`
|
||||
Value float64 `json:"value"`
|
||||
Time time.Time `json:"timestamp"`
|
||||
}
|
||||
|
||||
// FetchFromMock simula obtenção de dados de um oráculo externo (mock)
|
||||
func FetchFromMock(key string) (*OracleData, error) {
|
||||
mockPrices := map[string]float64{
|
||||
"USD-BRL": 5.06,
|
||||
"ETH-BRL": 18732.42,
|
||||
"IMOVEL-ABC123": 950000.00,
|
||||
}
|
||||
val, ok := mockPrices[key]
|
||||
if !ok {
|
||||
return nil, errors.New("dado não encontrado no mock")
|
||||
}
|
||||
return &OracleData{
|
||||
Source: "mock",
|
||||
Key: key,
|
||||
Value: val,
|
||||
Time: time.Now(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// FetchFromCoinGecko busca o preço de uma moeda em relação ao BRL
|
||||
func FetchFromCoinGecko(coin string) (*OracleData, error) {
|
||||
url := fmt.Sprintf("https://api.coingecko.com/api/v3/simple/price?ids=%s&vs_currencies=brl", coin)
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var parsed map[string]map[string]float64
|
||||
if err := json.NewDecoder(resp.Body).Decode(&parsed); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
price := parsed[coin]["brl"]
|
||||
|
||||
return &OracleData{
|
||||
Source: "coingecko",
|
||||
Key: coin + "-BRL",
|
||||
Value: price,
|
||||
Time: time.Now(),
|
||||
}, nil
|
||||
}
|
||||
20
internal/oracle/fetcher_test.go
Normal file
20
internal/oracle/fetcher_test.go
Normal file
@ -0,0 +1,20 @@
|
||||
package oracle
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFetchFromMock(t *testing.T) {
|
||||
d, err := FetchFromMock("USD-BRL")
|
||||
if err != nil {
|
||||
t.Fatalf("Erro ao buscar mock: %v", err)
|
||||
}
|
||||
if d.Key != "USD-BRL" || d.Value <= 0 {
|
||||
t.Errorf("Valor inesperado do mock: %+v", d)
|
||||
}
|
||||
|
||||
_, err = FetchFromMock("INVALIDO")
|
||||
if err == nil {
|
||||
t.Error("Esperado erro ao buscar chave inválida no mock")
|
||||
}
|
||||
}
|
||||
49
internal/oracle/store.go
Normal file
49
internal/oracle/store.go
Normal file
@ -0,0 +1,49 @@
|
||||
package oracle
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Store mantém os últimos dados válidos dos oráculos e um pequeno histórico
|
||||
|
||||
type Store struct {
|
||||
mu sync.RWMutex
|
||||
latest map[string]*OracleData
|
||||
history map[string][]*OracleData
|
||||
maxHist int
|
||||
}
|
||||
|
||||
// NewStore cria uma nova instância de armazenamento de oráculos
|
||||
func NewStore(maxHistory int) *Store {
|
||||
return &Store{
|
||||
latest: make(map[string]*OracleData),
|
||||
history: make(map[string][]*OracleData),
|
||||
maxHist: maxHistory,
|
||||
}
|
||||
}
|
||||
|
||||
// Save registra um novo dado e atualiza o histórico
|
||||
func (s *Store) Save(data *OracleData) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
s.latest[data.Key] = data
|
||||
s.history[data.Key] = append(s.history[data.Key], data)
|
||||
if len(s.history[data.Key]) > s.maxHist {
|
||||
s.history[data.Key] = s.history[data.Key][len(s.history[data.Key])-s.maxHist:]
|
||||
}
|
||||
}
|
||||
|
||||
// Get retorna o último dado registrado
|
||||
func (s *Store) Get(key string) *OracleData {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return s.latest[key]
|
||||
}
|
||||
|
||||
// History retorna o histórico completo de um dado
|
||||
func (s *Store) History(key string) []*OracleData {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return s.history[key]
|
||||
}
|
||||
30
internal/oracle/store_test.go
Normal file
30
internal/oracle/store_test.go
Normal file
@ -0,0 +1,30 @@
|
||||
package oracle
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestStore_SaveAndGet(t *testing.T) {
|
||||
s := NewStore(3)
|
||||
d := &OracleData{Key: "ETH-BRL", Value: 10000.0}
|
||||
|
||||
s.Save(d)
|
||||
|
||||
if got := s.Get("ETH-BRL"); got == nil || got.Value != 10000.0 {
|
||||
t.Error("falha ao buscar valor salvo")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStore_History(t *testing.T) {
|
||||
s := NewStore(2)
|
||||
|
||||
s.Save(&OracleData{Key: "ETH-BRL", Value: 1})
|
||||
s.Save(&OracleData{Key: "ETH-BRL", Value: 2})
|
||||
s.Save(&OracleData{Key: "ETH-BRL", Value: 3}) // deve descartar o primeiro
|
||||
|
||||
hist := s.History("ETH-BRL")
|
||||
if len(hist) != 2 {
|
||||
t.Errorf("esperado 2 entradas no histórico, obtido %d", len(hist))
|
||||
}
|
||||
if hist[0].Value != 2 || hist[1].Value != 3 {
|
||||
t.Error("histórico não está correto")
|
||||
}
|
||||
}
|
||||
45
internal/oracle/validator.go
Normal file
45
internal/oracle/validator.go
Normal file
@ -0,0 +1,45 @@
|
||||
package oracle
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ValidateMajority recebe múltiplas leituras e tenta achar um consenso baseado na maioria simples.
|
||||
func ValidateMajority(data []*OracleData, tolerance float64) (*OracleData, error) {
|
||||
if len(data) == 0 {
|
||||
return nil, fmt.Errorf("nenhum dado recebido para validar")
|
||||
}
|
||||
|
||||
count := make(map[float64]int)
|
||||
latest := make(map[float64]time.Time)
|
||||
|
||||
for _, d := range data {
|
||||
rounded := math.Round(d.Value/tolerance) * tolerance // agrupar valores similares
|
||||
count[rounded]++
|
||||
if d.Time.After(latest[rounded]) {
|
||||
latest[rounded] = d.Time
|
||||
}
|
||||
}
|
||||
|
||||
// Encontrar o valor com mais votos
|
||||
var maxVal float64
|
||||
var maxCount int
|
||||
for val, c := range count {
|
||||
if c > maxCount {
|
||||
maxVal = val
|
||||
maxCount = c
|
||||
}
|
||||
}
|
||||
|
||||
// Retornar o OracleData mais recente com esse valor
|
||||
for _, d := range data {
|
||||
rounded := math.Round(d.Value/tolerance) * tolerance
|
||||
if rounded == maxVal && d.Time.Equal(latest[maxVal]) {
|
||||
return d, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("nenhum consenso válido encontrado")
|
||||
}
|
||||
28
internal/oracle/validator_test.go
Normal file
28
internal/oracle/validator_test.go
Normal file
@ -0,0 +1,28 @@
|
||||
package oracle
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestValidateMajority(t *testing.T) {
|
||||
now := time.Now()
|
||||
d1 := &OracleData{Key: "USD-BRL", Value: 5.00, Time: now}
|
||||
d2 := &OracleData{Key: "USD-BRL", Value: 5.01, Time: now.Add(-1 * time.Minute)}
|
||||
d3 := &OracleData{Key: "USD-BRL", Value: 4.99, Time: now.Add(-2 * time.Minute)}
|
||||
|
||||
res, err := ValidateMajority([]*OracleData{d1, d2, d3}, 0.05)
|
||||
if err != nil {
|
||||
t.Fatalf("Falha ao validar maioria: %v", err)
|
||||
}
|
||||
if res == nil || res.Value == 0 {
|
||||
t.Error("Valor inválido retornado pelo consenso")
|
||||
}
|
||||
|
||||
// sem consenso
|
||||
d4 := &OracleData{Key: "USD-BRL", Value: 3.0, Time: now}
|
||||
_, err = ValidateMajority([]*OracleData{d1, d4}, 0.001)
|
||||
if err == nil {
|
||||
t.Error("Esperado erro por falta de consenso")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user