mirror of
https://github.com/luxfi/indexer.git
synced 2026-09-24 00:25:37 +00:00
The primary-network chains answer to either case; one spelling beats two. /v1/chain/c/rpc, /v1/chain/p, /v1/chain/x across the estate. Co-authored-by: Hanzo Dev <[email protected]>
600 lines
14 KiB
Go
600 lines
14 KiB
Go
// Copyright (c) 2025 Lux Industries Inc
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package utxo
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/luxfi/indexer/dag"
|
|
)
|
|
|
|
// TestNew tests adapter creation
|
|
func TestNew(t *testing.T) {
|
|
endpoint := "http://localhost:9650/v1/chain/x"
|
|
adapter := New(endpoint)
|
|
|
|
if adapter == nil {
|
|
t.Fatal("expected non-nil adapter")
|
|
}
|
|
if adapter.rpcEndpoint != endpoint {
|
|
t.Errorf("expected endpoint %s, got %s", endpoint, adapter.rpcEndpoint)
|
|
}
|
|
if adapter.client == nil {
|
|
t.Error("expected non-nil http client")
|
|
}
|
|
if adapter.client.Timeout != 30*time.Second {
|
|
t.Errorf("expected 30s timeout, got %v", adapter.client.Timeout)
|
|
}
|
|
}
|
|
|
|
// TestAdapterImplementsInterface verifies Adapter implements dag.Adapter
|
|
func TestAdapterImplementsInterface(t *testing.T) {
|
|
var _ dag.Adapter = (*Adapter)(nil)
|
|
}
|
|
|
|
// TestCallRPC tests JSON-RPC call functionality
|
|
func TestCallRPC(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
method string
|
|
params interface{}
|
|
response string
|
|
wantErr bool
|
|
errContain string
|
|
}{
|
|
{
|
|
name: "successful call",
|
|
method: "xvm.getRecentVertices",
|
|
params: map[string]int{"limit": 10},
|
|
response: `{
|
|
"jsonrpc": "2.0",
|
|
"id": 1,
|
|
"result": {"vertices": []}
|
|
}`,
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "rpc error response",
|
|
method: "xvm.invalidMethod",
|
|
params: nil,
|
|
response: `{
|
|
"jsonrpc": "2.0",
|
|
"id": 1,
|
|
"error": {"code": -32601, "message": "method not found"}
|
|
}`,
|
|
wantErr: true,
|
|
errContain: "method not found",
|
|
},
|
|
{
|
|
name: "invalid json response",
|
|
method: "xvm.test",
|
|
params: nil,
|
|
response: `invalid json`,
|
|
wantErr: true,
|
|
errContain: "decode response",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != "POST" {
|
|
t.Errorf("expected POST, got %s", r.Method)
|
|
}
|
|
if r.Header.Get("Content-Type") != "application/json" {
|
|
t.Errorf("expected application/json content type")
|
|
}
|
|
|
|
var req rpcRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
t.Errorf("failed to decode request: %v", err)
|
|
}
|
|
if req.JSONRPC != "2.0" {
|
|
t.Errorf("expected jsonrpc 2.0, got %s", req.JSONRPC)
|
|
}
|
|
if req.Method != tt.method {
|
|
t.Errorf("expected method %s, got %s", tt.method, req.Method)
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.Write([]byte(tt.response))
|
|
}))
|
|
defer server.Close()
|
|
|
|
adapter := New(server.URL)
|
|
result, err := adapter.callRPC(context.Background(), tt.method, tt.params)
|
|
|
|
if tt.wantErr {
|
|
if err == nil {
|
|
t.Error("expected error, got nil")
|
|
} else if tt.errContain != "" && !contains(err.Error(), tt.errContain) {
|
|
t.Errorf("error %q should contain %q", err.Error(), tt.errContain)
|
|
}
|
|
} else {
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
if result == nil {
|
|
t.Error("expected non-nil result")
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestCallRPCNetworkError tests network error handling
|
|
func TestCallRPCNetworkError(t *testing.T) {
|
|
adapter := New("http://localhost:99999/invalid")
|
|
_, err := adapter.callRPC(context.Background(), "xvm.test", nil)
|
|
if err == nil {
|
|
t.Error("expected network error")
|
|
}
|
|
}
|
|
|
|
// TestCallRPCContextCancellation tests context cancellation
|
|
func TestCallRPCContextCancellation(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
time.Sleep(100 * time.Millisecond)
|
|
w.Write([]byte(`{"jsonrpc":"2.0","id":1,"result":{}}`))
|
|
}))
|
|
defer server.Close()
|
|
|
|
adapter := New(server.URL)
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel() // Cancel immediately
|
|
|
|
_, err := adapter.callRPC(ctx, "xvm.test", nil)
|
|
if err == nil {
|
|
t.Error("expected context cancellation error")
|
|
}
|
|
}
|
|
|
|
// TestParseVertex tests vertex parsing
|
|
func TestParseVertex(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
data string
|
|
wantID string
|
|
wantType string
|
|
wantStatus dag.Status
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "accepted vertex",
|
|
data: `{
|
|
"id": "vtx123",
|
|
"parentIDs": ["vtx122", "vtx121"],
|
|
"height": 100,
|
|
"epoch": 5,
|
|
"txs": ["tx1", "tx2"],
|
|
"timestamp": 1700000000,
|
|
"status": "Accepted",
|
|
"chainID": "X"
|
|
}`,
|
|
wantID: "vtx123",
|
|
wantType: "xchain_vertex",
|
|
wantStatus: dag.StatusAccepted,
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "pending vertex",
|
|
data: `{
|
|
"id": "vtx124",
|
|
"parentIDs": [],
|
|
"height": 101,
|
|
"epoch": 5,
|
|
"txs": [],
|
|
"timestamp": 1700000001,
|
|
"status": "Processing",
|
|
"chainID": "X"
|
|
}`,
|
|
wantID: "vtx124",
|
|
wantType: "xchain_vertex",
|
|
wantStatus: dag.StatusPending,
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "rejected vertex",
|
|
data: `{
|
|
"id": "vtx125",
|
|
"parentIDs": ["vtx100"],
|
|
"height": 50,
|
|
"epoch": 2,
|
|
"txs": ["tx3"],
|
|
"timestamp": 1700000002,
|
|
"status": "Rejected",
|
|
"chainID": "X"
|
|
}`,
|
|
wantID: "vtx125",
|
|
wantType: "xchain_vertex",
|
|
wantStatus: dag.StatusRejected,
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "zero timestamp uses current time",
|
|
data: `{
|
|
"id": "vtx126",
|
|
"parentIDs": [],
|
|
"height": 102,
|
|
"epoch": 5,
|
|
"txs": [],
|
|
"timestamp": 0,
|
|
"status": "Accepted",
|
|
"chainID": "X"
|
|
}`,
|
|
wantID: "vtx126",
|
|
wantType: "xchain_vertex",
|
|
wantStatus: dag.StatusAccepted,
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "invalid json",
|
|
data: `{invalid json}`,
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
adapter := New("http://localhost:9650")
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
vertex, err := adapter.ParseVertex(json.RawMessage(tt.data))
|
|
|
|
if tt.wantErr {
|
|
if err == nil {
|
|
t.Error("expected error, got nil")
|
|
}
|
|
return
|
|
}
|
|
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if vertex.ID != tt.wantID {
|
|
t.Errorf("expected ID %s, got %s", tt.wantID, vertex.ID)
|
|
}
|
|
if vertex.Type != tt.wantType {
|
|
t.Errorf("expected Type %s, got %s", tt.wantType, vertex.Type)
|
|
}
|
|
if vertex.Status != tt.wantStatus {
|
|
t.Errorf("expected Status %v, got %v", tt.wantStatus, vertex.Status)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestGetRecentVertices tests fetching recent vertices
|
|
func TestGetRecentVertices(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
var req rpcRequest
|
|
json.NewDecoder(r.Body).Decode(&req)
|
|
|
|
if req.Method != "xvm.getRecentVertices" {
|
|
t.Errorf("expected method xvm.getRecentVertices, got %s", req.Method)
|
|
}
|
|
|
|
params := req.Params.(map[string]interface{})
|
|
limit := int(params["limit"].(float64))
|
|
|
|
vertices := make([]json.RawMessage, 0, limit)
|
|
for i := 0; i < limit && i < 3; i++ {
|
|
vertices = append(vertices, json.RawMessage(`{"id":"vtx`+string(rune('1'+i))+`"}`))
|
|
}
|
|
|
|
resp := map[string]interface{}{
|
|
"jsonrpc": "2.0",
|
|
"id": 1,
|
|
"result": map[string]interface{}{"vertices": vertices},
|
|
}
|
|
json.NewEncoder(w).Encode(resp)
|
|
}))
|
|
defer server.Close()
|
|
|
|
adapter := New(server.URL)
|
|
vertices, err := adapter.GetRecentVertices(context.Background(), 5)
|
|
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if len(vertices) != 3 {
|
|
t.Errorf("expected 3 vertices, got %d", len(vertices))
|
|
}
|
|
}
|
|
|
|
// TestGetVertexByID tests fetching a specific vertex
|
|
func TestGetVertexByID(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
vertexID string
|
|
response string
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "found vertex",
|
|
vertexID: "vtx123",
|
|
response: `{
|
|
"jsonrpc": "2.0",
|
|
"id": 1,
|
|
"result": {"vertex": {"id": "vtx123", "status": "Accepted"}}
|
|
}`,
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "vertex not found",
|
|
vertexID: "vtx999",
|
|
response: `{
|
|
"jsonrpc": "2.0",
|
|
"id": 1,
|
|
"error": {"code": -32000, "message": "vertex not found"}
|
|
}`,
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
var req rpcRequest
|
|
json.NewDecoder(r.Body).Decode(&req)
|
|
|
|
if req.Method != "xvm.getVertex" {
|
|
t.Errorf("expected method xvm.getVertex, got %s", req.Method)
|
|
}
|
|
|
|
w.Write([]byte(tt.response))
|
|
}))
|
|
defer server.Close()
|
|
|
|
adapter := New(server.URL)
|
|
vertex, err := adapter.GetVertexByID(context.Background(), tt.vertexID)
|
|
|
|
if tt.wantErr {
|
|
if err == nil {
|
|
t.Error("expected error, got nil")
|
|
}
|
|
} else {
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if vertex == nil {
|
|
t.Error("expected non-nil vertex")
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestUTXOStructure tests UTXO struct fields
|
|
func TestUTXOStructure(t *testing.T) {
|
|
utxo := UTXO{
|
|
ID: "tx1:0",
|
|
TxID: "tx1",
|
|
OutIndex: 0,
|
|
AssetID: "LUX",
|
|
Amount: 1000000000,
|
|
Address: "X-lux1abc...",
|
|
Spent: false,
|
|
SpentBy: "",
|
|
}
|
|
|
|
if utxo.ID != "tx1:0" {
|
|
t.Errorf("expected ID tx1:0, got %s", utxo.ID)
|
|
}
|
|
if utxo.Amount != 1000000000 {
|
|
t.Errorf("expected Amount 1000000000, got %d", utxo.Amount)
|
|
}
|
|
}
|
|
|
|
// TestAssetStructure tests Asset struct fields
|
|
func TestAssetStructure(t *testing.T) {
|
|
asset := Asset{
|
|
ID: "LUX",
|
|
Name: "Lux",
|
|
Symbol: "LUX",
|
|
Denomination: 9,
|
|
TotalSupply: 720000000000000000,
|
|
}
|
|
|
|
if asset.Symbol != "LUX" {
|
|
t.Errorf("expected Symbol LUX, got %s", asset.Symbol)
|
|
}
|
|
if asset.Denomination != 9 {
|
|
t.Errorf("expected Denomination 9, got %d", asset.Denomination)
|
|
}
|
|
}
|
|
|
|
// TestTransactionStructure tests Transaction struct fields
|
|
func TestTransactionStructure(t *testing.T) {
|
|
tx := Transaction{
|
|
ID: "tx123",
|
|
Type: "BaseTx",
|
|
Inputs: []Input{
|
|
{TxID: "tx100", OutIndex: 0, AssetID: "LUX", Amount: 1000, Address: "X-lux1..."},
|
|
},
|
|
Outputs: []Output{
|
|
{AssetID: "LUX", Amount: 900, Addresses: []string{"X-lux1..."}, Threshold: 1},
|
|
},
|
|
Memo: "test memo",
|
|
Timestamp: 1700000000,
|
|
}
|
|
|
|
if tx.Type != "BaseTx" {
|
|
t.Errorf("expected Type BaseTx, got %s", tx.Type)
|
|
}
|
|
if len(tx.Inputs) != 1 {
|
|
t.Errorf("expected 1 input, got %d", len(tx.Inputs))
|
|
}
|
|
if len(tx.Outputs) != 1 {
|
|
t.Errorf("expected 1 output, got %d", len(tx.Outputs))
|
|
}
|
|
}
|
|
|
|
// TestVertexDataStructure tests VertexData struct
|
|
func TestVertexDataStructure(t *testing.T) {
|
|
data := VertexData{
|
|
Transactions: []Transaction{
|
|
{ID: "tx1", Type: "BaseTx"},
|
|
{ID: "tx2", Type: "CreateAssetTx"},
|
|
},
|
|
ChainID: "X",
|
|
}
|
|
|
|
if len(data.Transactions) != 2 {
|
|
t.Errorf("expected 2 transactions, got %d", len(data.Transactions))
|
|
}
|
|
if data.ChainID != "X" {
|
|
t.Errorf("expected ChainID X, got %s", data.ChainID)
|
|
}
|
|
}
|
|
|
|
// TestRPCRequestSerialization tests JSON serialization of RPC request
|
|
func TestRPCRequestSerialization(t *testing.T) {
|
|
req := rpcRequest{
|
|
JSONRPC: "2.0",
|
|
ID: 1,
|
|
Method: "xvm.test",
|
|
Params: map[string]string{"key": "value"},
|
|
}
|
|
|
|
data, err := json.Marshal(req)
|
|
if err != nil {
|
|
t.Fatalf("failed to marshal: %v", err)
|
|
}
|
|
|
|
var decoded rpcRequest
|
|
if err := json.Unmarshal(data, &decoded); err != nil {
|
|
t.Fatalf("failed to unmarshal: %v", err)
|
|
}
|
|
|
|
if decoded.JSONRPC != "2.0" {
|
|
t.Errorf("expected jsonrpc 2.0, got %s", decoded.JSONRPC)
|
|
}
|
|
if decoded.Method != "xvm.test" {
|
|
t.Errorf("expected method xvm.test, got %s", decoded.Method)
|
|
}
|
|
}
|
|
|
|
// TestRPCErrorStructure tests RPC error structure
|
|
func TestRPCErrorStructure(t *testing.T) {
|
|
errResp := `{"code": -32601, "message": "method not found"}`
|
|
|
|
var rpcErr rpcError
|
|
if err := json.Unmarshal([]byte(errResp), &rpcErr); err != nil {
|
|
t.Fatalf("failed to unmarshal: %v", err)
|
|
}
|
|
|
|
if rpcErr.Code != -32601 {
|
|
t.Errorf("expected code -32601, got %d", rpcErr.Code)
|
|
}
|
|
if rpcErr.Message != "method not found" {
|
|
t.Errorf("expected message 'method not found', got %s", rpcErr.Message)
|
|
}
|
|
}
|
|
|
|
// TestInputOutputStructures tests Input and Output struct serialization
|
|
func TestInputOutputStructures(t *testing.T) {
|
|
input := Input{
|
|
TxID: "tx1",
|
|
OutIndex: 0,
|
|
AssetID: "LUX",
|
|
Amount: 1000,
|
|
Address: "X-lux1abc",
|
|
}
|
|
|
|
output := Output{
|
|
AssetID: "LUX",
|
|
Amount: 900,
|
|
Addresses: []string{"X-lux1def", "X-lux1ghi"},
|
|
Threshold: 2,
|
|
Locktime: 1700000000,
|
|
}
|
|
|
|
// Test JSON serialization
|
|
inputJSON, err := json.Marshal(input)
|
|
if err != nil {
|
|
t.Fatalf("failed to marshal input: %v", err)
|
|
}
|
|
|
|
outputJSON, err := json.Marshal(output)
|
|
if err != nil {
|
|
t.Fatalf("failed to marshal output: %v", err)
|
|
}
|
|
|
|
var decodedInput Input
|
|
if err := json.Unmarshal(inputJSON, &decodedInput); err != nil {
|
|
t.Fatalf("failed to unmarshal input: %v", err)
|
|
}
|
|
|
|
var decodedOutput Output
|
|
if err := json.Unmarshal(outputJSON, &decodedOutput); err != nil {
|
|
t.Fatalf("failed to unmarshal output: %v", err)
|
|
}
|
|
|
|
if decodedInput.TxID != "tx1" {
|
|
t.Errorf("expected TxID tx1, got %s", decodedInput.TxID)
|
|
}
|
|
if decodedOutput.Threshold != 2 {
|
|
t.Errorf("expected Threshold 2, got %d", decodedOutput.Threshold)
|
|
}
|
|
if len(decodedOutput.Addresses) != 2 {
|
|
t.Errorf("expected 2 addresses, got %d", len(decodedOutput.Addresses))
|
|
}
|
|
}
|
|
|
|
// helper function
|
|
func contains(s, substr string) bool {
|
|
return len(s) >= len(substr) && (s == substr || len(s) > 0 && containsAt(s, substr, 0))
|
|
}
|
|
|
|
func containsAt(s, substr string, start int) bool {
|
|
for i := start; i <= len(s)-len(substr); i++ {
|
|
if s[i:i+len(substr)] == substr {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// BenchmarkParseVertex benchmarks vertex parsing
|
|
func BenchmarkParseVertex(b *testing.B) {
|
|
adapter := New("http://localhost:9650")
|
|
data := json.RawMessage(`{
|
|
"id": "vtx123",
|
|
"parentIDs": ["vtx122", "vtx121"],
|
|
"height": 100,
|
|
"epoch": 5,
|
|
"txs": ["tx1", "tx2", "tx3"],
|
|
"timestamp": 1700000000,
|
|
"status": "Accepted",
|
|
"chainID": "X"
|
|
}`)
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
_, _ = adapter.ParseVertex(data)
|
|
}
|
|
}
|
|
|
|
// BenchmarkCallRPC benchmarks RPC calls
|
|
func BenchmarkCallRPC(b *testing.B) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Write([]byte(`{"jsonrpc":"2.0","id":1,"result":{"vertices":[]}}`))
|
|
}))
|
|
defer server.Close()
|
|
|
|
adapter := New(server.URL)
|
|
ctx := context.Background()
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
_, _ = adapter.callRPC(ctx, "xvm.getRecentVertices", map[string]int{"limit": 10})
|
|
}
|
|
}
|