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]>
623 lines
18 KiB
Go
623 lines
18 KiB
Go
// Copyright (c) 2025 Lux Industries Inc
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
// Package pchain provides the P-Chain (Platform) adapter for the LINEAR chain indexer.
|
|
// Handles validators, delegators, chains, and staking operations.
|
|
package platform
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/luxfi/indexer/chain"
|
|
"github.com/luxfi/indexer/storage"
|
|
)
|
|
|
|
const (
|
|
DefaultRPCEndpoint = "http://localhost:9650/v1/chain/p"
|
|
DefaultHTTPPort = 4100
|
|
)
|
|
|
|
// Adapter implements chain.Adapter for P-Chain
|
|
type Adapter struct {
|
|
rpcEndpoint string
|
|
httpClient *http.Client
|
|
}
|
|
|
|
// New creates a new P-Chain adapter
|
|
func New(rpcEndpoint string) *Adapter {
|
|
if rpcEndpoint == "" {
|
|
rpcEndpoint = DefaultRPCEndpoint
|
|
}
|
|
return &Adapter{
|
|
rpcEndpoint: rpcEndpoint,
|
|
httpClient: &http.Client{Timeout: 30 * time.Second},
|
|
}
|
|
}
|
|
|
|
// rpcRequest makes a JSON-RPC call to the P-Chain
|
|
func (a *Adapter) rpcRequest(ctx context.Context, method string, params interface{}) (json.RawMessage, error) {
|
|
reqBody := map[string]interface{}{
|
|
"jsonrpc": "2.0",
|
|
"id": 1,
|
|
"method": method,
|
|
"params": params,
|
|
}
|
|
body, err := json.Marshal(reqBody)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("marshal request: %w", err)
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, "POST", a.rpcEndpoint, bytes.NewReader(body))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create request: %w", err)
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
resp, err := a.httpClient.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("rpc call: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
respBody, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read response: %w", err)
|
|
}
|
|
|
|
var rpcResp struct {
|
|
Result json.RawMessage `json:"result"`
|
|
Error *struct {
|
|
Code int `json:"code"`
|
|
Message string `json:"message"`
|
|
} `json:"error"`
|
|
}
|
|
if err := json.Unmarshal(respBody, &rpcResp); err != nil {
|
|
return nil, fmt.Errorf("unmarshal response: %w", err)
|
|
}
|
|
if rpcResp.Error != nil {
|
|
return nil, fmt.Errorf("rpc error %d: %s", rpcResp.Error.Code, rpcResp.Error.Message)
|
|
}
|
|
|
|
return rpcResp.Result, nil
|
|
}
|
|
|
|
// PChainBlock represents a P-Chain block from RPC
|
|
type PChainBlock struct {
|
|
ID string `json:"id"`
|
|
ParentID string `json:"parentID"`
|
|
Height uint64 `json:"height"`
|
|
Timestamp int64 `json:"timestamp"` // Unix timestamp
|
|
Txs []PChainTx `json:"txs"`
|
|
}
|
|
|
|
// PChainTx represents a P-Chain transaction
|
|
type PChainTx struct {
|
|
ID string `json:"id"`
|
|
Type string `json:"type"`
|
|
Inputs json.RawMessage `json:"inputs,omitempty"`
|
|
Outputs json.RawMessage `json:"outputs,omitempty"`
|
|
Credentials json.RawMessage `json:"credentials,omitempty"`
|
|
// Validator-specific fields
|
|
NodeID string `json:"nodeID,omitempty"`
|
|
StartTime int64 `json:"startTime,omitempty"`
|
|
EndTime int64 `json:"endTime,omitempty"`
|
|
Weight uint64 `json:"weight,omitempty"`
|
|
NetID string `json:"netID,omitempty"` // For chain operations
|
|
}
|
|
|
|
// ParseBlock implements chain.Adapter
|
|
func (a *Adapter) ParseBlock(data json.RawMessage) (*chain.Block, error) {
|
|
var pb PChainBlock
|
|
if err := json.Unmarshal(data, &pb); err != nil {
|
|
return nil, fmt.Errorf("parse pchain block: %w", err)
|
|
}
|
|
|
|
txIDs := make([]string, len(pb.Txs))
|
|
for i, tx := range pb.Txs {
|
|
txIDs[i] = tx.ID
|
|
}
|
|
|
|
metadata := map[string]interface{}{
|
|
"txTypes": extractTxTypes(pb.Txs),
|
|
}
|
|
|
|
return &chain.Block{
|
|
ID: pb.ID,
|
|
ParentID: pb.ParentID,
|
|
Height: pb.Height,
|
|
Timestamp: time.Unix(pb.Timestamp, 0),
|
|
Status: chain.StatusAccepted,
|
|
TxCount: len(pb.Txs),
|
|
TxIDs: txIDs,
|
|
Data: data,
|
|
Metadata: metadata,
|
|
}, nil
|
|
}
|
|
|
|
func extractTxTypes(txs []PChainTx) map[string]int {
|
|
types := make(map[string]int)
|
|
for _, tx := range txs {
|
|
types[tx.Type]++
|
|
}
|
|
return types
|
|
}
|
|
|
|
// GetRecentBlocks implements chain.Adapter
|
|
func (a *Adapter) GetRecentBlocks(ctx context.Context, limit int) ([]json.RawMessage, error) {
|
|
// Get current height first
|
|
heightResult, err := a.rpcRequest(ctx, "platform.getHeight", map[string]interface{}{})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get height: %w", err)
|
|
}
|
|
|
|
var heightResp struct {
|
|
Height string `json:"height"`
|
|
}
|
|
if err := json.Unmarshal(heightResult, &heightResp); err != nil {
|
|
return nil, fmt.Errorf("parse height: %w", err)
|
|
}
|
|
|
|
var currentHeight uint64
|
|
_, _ = fmt.Sscanf(heightResp.Height, "%d", ¤tHeight)
|
|
|
|
blocks := make([]json.RawMessage, 0, limit)
|
|
for i := 0; i < limit && currentHeight >= uint64(i); i++ {
|
|
height := currentHeight - uint64(i)
|
|
blockData, err := a.GetBlockByHeight(ctx, height)
|
|
if err != nil {
|
|
continue // Skip failed blocks
|
|
}
|
|
blocks = append(blocks, blockData)
|
|
}
|
|
|
|
return blocks, nil
|
|
}
|
|
|
|
// GetBlockByID implements chain.Adapter
|
|
func (a *Adapter) GetBlockByID(ctx context.Context, id string) (json.RawMessage, error) {
|
|
result, err := a.rpcRequest(ctx, "platform.getBlock", map[string]interface{}{
|
|
"blockID": id,
|
|
"encoding": "json",
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get block by id: %w", err)
|
|
}
|
|
|
|
var blockResp struct {
|
|
Block json.RawMessage `json:"block"`
|
|
}
|
|
if err := json.Unmarshal(result, &blockResp); err != nil {
|
|
return nil, fmt.Errorf("parse block response: %w", err)
|
|
}
|
|
|
|
return blockResp.Block, nil
|
|
}
|
|
|
|
// GetBlockByHeight implements chain.Adapter
|
|
func (a *Adapter) GetBlockByHeight(ctx context.Context, height uint64) (json.RawMessage, error) {
|
|
result, err := a.rpcRequest(ctx, "platform.getBlockByHeight", map[string]interface{}{
|
|
"height": fmt.Sprintf("%d", height),
|
|
"encoding": "json",
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get block at height %d: %w", height, err)
|
|
}
|
|
|
|
var blockResp struct {
|
|
Block json.RawMessage `json:"block"`
|
|
}
|
|
if err := json.Unmarshal(result, &blockResp); err != nil {
|
|
return nil, fmt.Errorf("parse block response: %w", err)
|
|
}
|
|
|
|
return blockResp.Block, nil
|
|
}
|
|
|
|
// InitSchema implements chain.Adapter - creates P-Chain specific tables
|
|
func (a *Adapter) InitSchema(ctx context.Context, store storage.Store) error {
|
|
schema := `
|
|
-- Validators table
|
|
CREATE TABLE IF NOT EXISTS pchain_validators (
|
|
node_id TEXT PRIMARY KEY,
|
|
start_time TIMESTAMP NOT NULL,
|
|
end_time TIMESTAMP NOT NULL,
|
|
stake_amount BIGINT NOT NULL,
|
|
potential_reward BIGINT DEFAULT 0,
|
|
delegation_fee REAL DEFAULT 0,
|
|
uptime REAL DEFAULT 0,
|
|
connected BOOLEAN DEFAULT false,
|
|
net_id TEXT DEFAULT 'primary',
|
|
tx_id TEXT,
|
|
bls_public_key TEXT DEFAULT '',
|
|
bls_proof_of_possession TEXT DEFAULT '',
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_pchain_validators_net ON pchain_validators(net_id);
|
|
CREATE INDEX IF NOT EXISTS idx_pchain_validators_end_time ON pchain_validators(end_time);
|
|
|
|
-- Delegators table
|
|
CREATE TABLE IF NOT EXISTS pchain_delegators (
|
|
tx_id TEXT PRIMARY KEY,
|
|
node_id TEXT NOT NULL,
|
|
start_time TIMESTAMP NOT NULL,
|
|
end_time TIMESTAMP NOT NULL,
|
|
stake_amount BIGINT NOT NULL,
|
|
potential_reward BIGINT DEFAULT 0,
|
|
reward_owner TEXT,
|
|
net_id TEXT DEFAULT 'primary',
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_pchain_delegators_node ON pchain_delegators(node_id);
|
|
CREATE INDEX IF NOT EXISTS idx_pchain_delegators_end ON pchain_delegators(end_time);
|
|
|
|
-- Networks table (one row per L1 / chain registered on the P-chain)
|
|
CREATE TABLE IF NOT EXISTS pchain_nets (
|
|
net_id TEXT PRIMARY KEY,
|
|
owner_addresses TEXT DEFAULT '[]',
|
|
threshold INT DEFAULT 1,
|
|
control_keys TEXT DEFAULT '[]',
|
|
tx_id TEXT,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
-- Blockchains table
|
|
CREATE TABLE IF NOT EXISTS pchain_chains (
|
|
chain_id TEXT PRIMARY KEY,
|
|
net_id TEXT,
|
|
name TEXT NOT NULL,
|
|
vm_id TEXT NOT NULL,
|
|
genesis_data TEXT,
|
|
tx_id TEXT,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_pchain_chains_net ON pchain_chains(net_id);
|
|
|
|
-- Staking rewards table
|
|
CREATE TABLE IF NOT EXISTS pchain_rewards (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
tx_id TEXT NOT NULL,
|
|
node_id TEXT,
|
|
delegator_tx_id TEXT,
|
|
amount BIGINT NOT NULL,
|
|
reward_type TEXT NOT NULL,
|
|
claimed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_pchain_rewards_node ON pchain_rewards(node_id);
|
|
|
|
-- P-Chain transactions table (extended)
|
|
CREATE TABLE IF NOT EXISTS pchain_txs (
|
|
tx_id TEXT PRIMARY KEY,
|
|
block_id TEXT,
|
|
tx_type TEXT NOT NULL,
|
|
inputs TEXT DEFAULT '[]',
|
|
outputs TEXT DEFAULT '[]',
|
|
memo TEXT,
|
|
fee BIGINT DEFAULT 0,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_pchain_txs_type ON pchain_txs(tx_type);
|
|
CREATE INDEX IF NOT EXISTS idx_pchain_txs_block ON pchain_txs(block_id);
|
|
|
|
-- Extended stats for P-Chain
|
|
CREATE TABLE IF NOT EXISTS pchain_extended_stats (
|
|
id INT PRIMARY KEY DEFAULT 1,
|
|
total_validators INT DEFAULT 0,
|
|
active_validators INT DEFAULT 0,
|
|
total_delegators INT DEFAULT 0,
|
|
total_stake BIGINT DEFAULT 0,
|
|
total_delegated BIGINT DEFAULT 0,
|
|
total_nets INT DEFAULT 0,
|
|
total_chains INT DEFAULT 0,
|
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
INSERT INTO pchain_extended_stats (id) VALUES (1) ON CONFLICT (id) DO NOTHING;
|
|
`
|
|
|
|
if err := store.Exec(ctx, schema); err != nil {
|
|
return fmt.Errorf("pchain schema: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetStats implements chain.Adapter - returns P-Chain specific statistics
|
|
func (a *Adapter) GetStats(ctx context.Context, store storage.Store) (map[string]interface{}, error) {
|
|
stats := make(map[string]interface{})
|
|
|
|
// Get validator stats
|
|
validatorRows, _ := store.Query(ctx, "SELECT COUNT(*) as cnt FROM pchain_validators")
|
|
activeRows, _ := store.Query(ctx, "SELECT COUNT(*) as cnt FROM pchain_validators WHERE end_time > NOW()")
|
|
stakeRows, _ := store.Query(ctx, "SELECT COALESCE(SUM(stake_amount), 0) as total FROM pchain_validators WHERE end_time > NOW()")
|
|
|
|
var totalValidators, activeValidators int64
|
|
var totalStake int64
|
|
if len(validatorRows) > 0 {
|
|
if v, ok := validatorRows[0]["cnt"].(int64); ok {
|
|
totalValidators = v
|
|
}
|
|
}
|
|
if len(activeRows) > 0 {
|
|
if v, ok := activeRows[0]["cnt"].(int64); ok {
|
|
activeValidators = v
|
|
}
|
|
}
|
|
if len(stakeRows) > 0 {
|
|
if v, ok := stakeRows[0]["total"].(int64); ok {
|
|
totalStake = v
|
|
}
|
|
}
|
|
|
|
stats["validators"] = map[string]interface{}{
|
|
"total": totalValidators,
|
|
"active": activeValidators,
|
|
"total_stake": totalStake,
|
|
}
|
|
|
|
// Get delegator stats
|
|
delegatorRows, _ := store.Query(ctx, "SELECT COUNT(*) as cnt FROM pchain_delegators WHERE end_time > NOW()")
|
|
delegatedRows, _ := store.Query(ctx, "SELECT COALESCE(SUM(stake_amount), 0) as total FROM pchain_delegators WHERE end_time > NOW()")
|
|
|
|
var totalDelegators int64
|
|
var totalDelegated int64
|
|
if len(delegatorRows) > 0 {
|
|
if v, ok := delegatorRows[0]["cnt"].(int64); ok {
|
|
totalDelegators = v
|
|
}
|
|
}
|
|
if len(delegatedRows) > 0 {
|
|
if v, ok := delegatedRows[0]["total"].(int64); ok {
|
|
totalDelegated = v
|
|
}
|
|
}
|
|
|
|
stats["delegators"] = map[string]interface{}{
|
|
"active": totalDelegators,
|
|
"total_delegated": totalDelegated,
|
|
}
|
|
|
|
// Get network/chain stats
|
|
netRows, _ := store.Query(ctx, "SELECT COUNT(*) as cnt FROM pchain_nets")
|
|
chainRows, _ := store.Query(ctx, "SELECT COUNT(*) as cnt FROM pchain_chains")
|
|
|
|
var totalNets, totalChains int64
|
|
if len(netRows) > 0 {
|
|
if v, ok := netRows[0]["cnt"].(int64); ok {
|
|
totalNets = v
|
|
}
|
|
}
|
|
if len(chainRows) > 0 {
|
|
if v, ok := chainRows[0]["cnt"].(int64); ok {
|
|
totalChains = v
|
|
}
|
|
}
|
|
|
|
stats["networks"] = map[string]interface{}{
|
|
"total_nets": totalNets,
|
|
"total_chains": totalChains,
|
|
}
|
|
|
|
return stats, nil
|
|
}
|
|
|
|
// GetCurrentValidators fetches current validators from RPC
|
|
func (a *Adapter) GetCurrentValidators(ctx context.Context, netID string) ([]Validator, error) {
|
|
params := map[string]interface{}{}
|
|
if netID != "" && netID != "primary" {
|
|
params["netID"] = netID
|
|
}
|
|
|
|
result, err := a.rpcRequest(ctx, "platform.getCurrentValidators", params)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get current validators: %w", err)
|
|
}
|
|
|
|
var resp struct {
|
|
Validators []Validator `json:"validators"`
|
|
}
|
|
if err := json.Unmarshal(result, &resp); err != nil {
|
|
return nil, fmt.Errorf("parse validators: %w", err)
|
|
}
|
|
|
|
return resp.Validators, nil
|
|
}
|
|
|
|
// GetPendingValidators fetches pending validators from RPC
|
|
func (a *Adapter) GetPendingValidators(ctx context.Context, netID string) ([]Validator, error) {
|
|
params := map[string]interface{}{}
|
|
if netID != "" && netID != "primary" {
|
|
params["netID"] = netID
|
|
}
|
|
|
|
result, err := a.rpcRequest(ctx, "platform.getPendingValidators", params)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get pending validators: %w", err)
|
|
}
|
|
|
|
var resp struct {
|
|
Validators []Validator `json:"validators"`
|
|
}
|
|
if err := json.Unmarshal(result, &resp); err != nil {
|
|
return nil, fmt.Errorf("parse pending validators: %w", err)
|
|
}
|
|
|
|
return resp.Validators, nil
|
|
}
|
|
|
|
// Validator represents a P-Chain validator
|
|
type Validator struct {
|
|
TxID string `json:"txID"`
|
|
NodeID string `json:"nodeID"`
|
|
StartTime string `json:"startTime"`
|
|
EndTime string `json:"endTime"`
|
|
Weight string `json:"weight"`
|
|
StakeAmount string `json:"stakeAmount,omitempty"` // Deprecated, use Weight
|
|
PotentialReward string `json:"potentialReward"`
|
|
DelegationFee string `json:"delegationFee"`
|
|
Uptime string `json:"uptime"`
|
|
Connected bool `json:"connected"`
|
|
Signer *Signer `json:"signer,omitempty"`
|
|
Delegators []Delegator `json:"delegators,omitempty"`
|
|
}
|
|
|
|
// Signer is the BLS proof-of-possession a permissionless validator registers
|
|
// on the P-Chain (platform.getCurrentValidators "signer" object).
|
|
type Signer struct {
|
|
PublicKey string `json:"publicKey"`
|
|
ProofOfPossession string `json:"proofOfPossession"`
|
|
}
|
|
|
|
// Delegator represents a P-Chain delegator
|
|
type Delegator struct {
|
|
TxID string `json:"txID"`
|
|
NodeID string `json:"nodeID"`
|
|
StartTime string `json:"startTime"`
|
|
EndTime string `json:"endTime"`
|
|
StakeAmount string `json:"stakeAmount"`
|
|
PotentialReward string `json:"potentialReward"`
|
|
RewardOwner string `json:"rewardOwner,omitempty"`
|
|
}
|
|
|
|
// GetNets fetches all chains from RPC.
|
|
func (a *Adapter) GetNets(ctx context.Context) ([]Net, error) {
|
|
result, err := a.rpcRequest(ctx, "platform.getBlockchains", map[string]interface{}{})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get chains: %w", err)
|
|
}
|
|
|
|
var resp struct {
|
|
Blockchains []Net `json:"blockchains"`
|
|
}
|
|
if err := json.Unmarshal(result, &resp); err != nil {
|
|
return nil, fmt.Errorf("parse chains: %w", err)
|
|
}
|
|
|
|
return resp.Blockchains, nil
|
|
}
|
|
|
|
// Net represents a P-Chain network/chain
|
|
type Net struct {
|
|
ID string `json:"id"`
|
|
ControlKeys []string `json:"controlKeys"`
|
|
Threshold string `json:"threshold"`
|
|
}
|
|
|
|
// GetBlockchains fetches all blockchains from RPC
|
|
func (a *Adapter) GetBlockchains(ctx context.Context) ([]Blockchain, error) {
|
|
result, err := a.rpcRequest(ctx, "platform.getBlockchains", map[string]interface{}{})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get blockchains: %w", err)
|
|
}
|
|
|
|
var resp struct {
|
|
Blockchains []Blockchain `json:"blockchains"`
|
|
}
|
|
if err := json.Unmarshal(result, &resp); err != nil {
|
|
return nil, fmt.Errorf("parse blockchains: %w", err)
|
|
}
|
|
|
|
return resp.Blockchains, nil
|
|
}
|
|
|
|
// Blockchain represents a P-Chain blockchain
|
|
type Blockchain struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
NetID string `json:"netID"`
|
|
VMID string `json:"vmID"`
|
|
}
|
|
|
|
// SyncValidators syncs validator data from RPC to database.
|
|
//
|
|
// Timestamps are bound as time.Time values (not Postgres-only to_timestamp())
|
|
// so the same statement runs on both the SQLite and Postgres query backends —
|
|
// each driver accepts a time.Time bind for a TIMESTAMP column. Weight falls
|
|
// back into stake_amount when the RPC omits the deprecated stakeAmount field,
|
|
// and the validator's BLS signer (publicKey + proofOfPossession) is captured.
|
|
func (a *Adapter) SyncValidators(ctx context.Context, store storage.Store) error {
|
|
validators, err := a.GetCurrentValidators(ctx, "")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, v := range validators {
|
|
stake := v.StakeAmount
|
|
if stake == "" {
|
|
stake = v.Weight
|
|
}
|
|
var blsPubKey, blsPoP string
|
|
if v.Signer != nil {
|
|
blsPubKey = v.Signer.PublicKey
|
|
blsPoP = v.Signer.ProofOfPossession
|
|
}
|
|
err := store.Exec(ctx, `
|
|
INSERT INTO pchain_validators (node_id, start_time, end_time, stake_amount, potential_reward, delegation_fee, uptime, connected, tx_id, bls_public_key, bls_proof_of_possession, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
|
ON CONFLICT (node_id) DO UPDATE SET
|
|
end_time = EXCLUDED.end_time,
|
|
stake_amount = EXCLUDED.stake_amount,
|
|
potential_reward = EXCLUDED.potential_reward,
|
|
delegation_fee = EXCLUDED.delegation_fee,
|
|
uptime = EXCLUDED.uptime,
|
|
connected = EXCLUDED.connected,
|
|
bls_public_key = EXCLUDED.bls_public_key,
|
|
bls_proof_of_possession = EXCLUDED.bls_proof_of_possession,
|
|
updated_at = CURRENT_TIMESTAMP
|
|
`, v.NodeID, parseUnixTime(v.StartTime), parseUnixTime(v.EndTime), stake, v.PotentialReward, v.DelegationFee, v.Uptime, v.Connected, v.TxID, blsPubKey, blsPoP)
|
|
if err != nil {
|
|
return fmt.Errorf("upsert validator %s: %w", v.NodeID, err)
|
|
}
|
|
|
|
// Sync delegators for this validator
|
|
for _, d := range v.Delegators {
|
|
err := store.Exec(ctx, `
|
|
INSERT INTO pchain_delegators (tx_id, node_id, start_time, end_time, stake_amount, potential_reward, reward_owner)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
ON CONFLICT (tx_id) DO UPDATE SET
|
|
end_time = EXCLUDED.end_time,
|
|
potential_reward = EXCLUDED.potential_reward
|
|
`, d.TxID, d.NodeID, parseUnixTime(d.StartTime), parseUnixTime(d.EndTime), d.StakeAmount, d.PotentialReward, d.RewardOwner)
|
|
if err != nil {
|
|
return fmt.Errorf("upsert delegator %s: %w", d.TxID, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// parseUnixTime converts a P-Chain RPC timestamp (Unix seconds encoded as a
|
|
// decimal string) into a UTC time.Time. Empty or unparseable input yields the
|
|
// zero time so a malformed field never aborts a whole sync.
|
|
func parseUnixTime(s string) time.Time {
|
|
if s == "" {
|
|
return time.Time{}
|
|
}
|
|
sec, err := strconv.ParseInt(s, 10, 64)
|
|
if err != nil {
|
|
return time.Time{}
|
|
}
|
|
return time.Unix(sec, 0).UTC()
|
|
}
|
|
|
|
// NewConfig creates a default P-Chain indexer configuration
|
|
func NewConfig() chain.Config {
|
|
return chain.Config{
|
|
ChainType: chain.ChainP,
|
|
ChainName: "P-Chain (Platform)",
|
|
RPCEndpoint: DefaultRPCEndpoint,
|
|
RPCMethod: "pvm",
|
|
HTTPPort: DefaultHTTPPort,
|
|
PollInterval: 5 * time.Second,
|
|
}
|
|
}
|
|
|
|
// Ensure Adapter implements chain.Adapter
|
|
var _ chain.Adapter = (*Adapter)(nil)
|