commit inicial do projeto

This commit is contained in:
Júnior
2025-05-23 10:44:32 -03:00
commit 8f04473c0b
106 changed files with 5673 additions and 0 deletions

View 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
}

View 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
View 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]
}

View 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")
}
}

View 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")
}

View 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")
}
}