init: wip indexer

This commit is contained in:
2024-04-23 19:33:05 +08:00
commit d0f21b2fdd
14 changed files with 1011 additions and 0 deletions

36
internal/event/event.go Normal file
View File

@@ -0,0 +1,36 @@
package event
import "encoding/json"
type (
Event struct {
Block uint64 `json:"block"`
ContractAddress string `json:"contractAddress"`
Success bool `json:"success"`
Timestamp int64 `json:"timestamp"`
TxHash string `json:"transactionHash"`
TxType string `json:"transactionType"`
Payload map[string]any `json:"payload"`
}
)
func (e Event) Serialize() ([]byte, error) {
jsonData, err := json.Marshal(e)
if err != nil {
return nil, err
}
return jsonData, err
}
func Deserialize(jsonData []byte) (Event, error) {
var (
event Event
)
if err := json.Unmarshal(jsonData, &event); err != nil {
return event, err
}
return event, nil
}

287
internal/store/pg.go Normal file
View File

@@ -0,0 +1,287 @@
package store
import (
"context"
"fmt"
"log/slog"
"os"
"time"
"github.com/grassrootseconomics/celo-indexer/internal/event"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/jackc/tern/v2/migrate"
"github.com/knadh/goyesql/v2"
)
type (
PgOpts struct {
DSN string
MigrationsFolderPath string
QueriesFolderPath string
Logg *slog.Logger
}
Pg struct {
db *pgxpool.Pool
queries *queries
logg *slog.Logger
}
queries struct {
InsertTx string `query:"insert-tx"`
InsertTokenTransfer string `query:"insert-token-transfer"`
InsertTokenMint string `query:"insert-token-mint"`
InsertPoolSwap string `query:"insert-pool-swap"`
InsertPoolDeposit string `query:"insert-pool-deposit"`
}
)
const (
migratorTimeout = 5 * time.Second
)
func NewPgStore(o PgOpts) (Store, error) {
parsedConfig, err := pgxpool.ParseConfig(o.DSN)
if err != nil {
return nil, err
}
dbPool, err := pgxpool.NewWithConfig(context.Background(), parsedConfig)
if err != nil {
return nil, err
}
queries, err := loadQueries(o.QueriesFolderPath)
if err != nil {
return nil, err
}
if err := runMigrations(context.Background(), dbPool, o.MigrationsFolderPath); err != nil {
return nil, err
}
return &Pg{
db: dbPool,
queries: queries,
logg: o.Logg,
}, nil
}
func (pg *Pg) InsertTokenTransfer(ctx context.Context, eventPayload event.Event) error {
tx, err := pg.db.Begin(ctx)
if err != nil {
pg.logg.Error("ERR0")
return err
}
defer func() {
if err != nil {
tx.Rollback(ctx)
} else {
tx.Commit(ctx)
}
}()
var (
txID int
)
if err := tx.QueryRow(
ctx,
pg.queries.InsertTx,
eventPayload.TxHash,
eventPayload.Block,
eventPayload.ContractAddress,
time.Unix(eventPayload.Timestamp, 0).UTC(),
eventPayload.Success,
).Scan(&txID); err != nil {
pg.logg.Error("ERR1")
return err
}
_, err = tx.Exec(
ctx,
pg.queries.InsertTokenTransfer,
txID,
eventPayload.Payload["from"].(string),
eventPayload.Payload["to"].(string),
eventPayload.Payload["value"].(string),
)
if err != nil {
pg.logg.Error("ERR2")
return err
}
return nil
}
func (pg *Pg) InsertTokenMint(ctx context.Context, eventPayload event.Event) error {
tx, err := pg.db.Begin(ctx)
if err != nil {
return err
}
defer func() {
if err != nil {
tx.Rollback(ctx)
} else {
tx.Commit(ctx)
}
}()
var (
txID int
)
if err := tx.QueryRow(
ctx,
pg.queries.InsertTx,
eventPayload.TxHash,
eventPayload.Block,
eventPayload.ContractAddress,
time.Unix(eventPayload.Timestamp, 0).UTC(),
eventPayload.Success,
).Scan(&txID); err != nil {
return err
}
_, err = tx.Exec(
ctx,
pg.queries.InsertTokenMint,
txID,
eventPayload.Payload["tokenMinter"].(string),
eventPayload.Payload["to"].(string),
eventPayload.Payload["value"].(string),
)
if err != nil {
return err
}
return nil
}
func (pg *Pg) InsertPoolSwap(ctx context.Context, eventPayload event.Event) error {
tx, err := pg.db.Begin(ctx)
if err != nil {
return err
}
defer func() {
if err != nil {
tx.Rollback(ctx)
} else {
tx.Commit(ctx)
}
}()
var (
txID int
)
if err := tx.QueryRow(
ctx,
pg.queries.InsertTx,
eventPayload.TxHash,
eventPayload.Block,
eventPayload.ContractAddress,
time.Unix(eventPayload.Timestamp, 0).UTC(),
eventPayload.Success,
).Scan(&txID); err != nil {
return err
}
_, err = tx.Exec(
ctx,
pg.queries.InsertPoolSwap,
txID,
eventPayload.Payload["initiator"].(string),
eventPayload.Payload["tokenIn"].(string),
eventPayload.Payload["tokenOut"].(string),
eventPayload.Payload["amountIn"].(string),
eventPayload.Payload["amountOut"].(string),
eventPayload.Payload["fee"].(string),
)
if err != nil {
return err
}
return nil
}
func (pg *Pg) InsertPoolDeposit(ctx context.Context, eventPayload event.Event) error {
tx, err := pg.db.Begin(ctx)
if err != nil {
return err
}
defer func() {
if err != nil {
tx.Rollback(ctx)
} else {
tx.Commit(ctx)
}
}()
var (
txID int
)
if err := tx.QueryRow(
ctx,
pg.queries.InsertTx,
eventPayload.TxHash,
eventPayload.Block,
eventPayload.ContractAddress,
time.Unix(eventPayload.Timestamp, 0).UTC(),
eventPayload.Success,
).Scan(&txID); err != nil {
return err
}
_, err = tx.Exec(
ctx,
pg.queries.InsertPoolDeposit,
txID,
eventPayload.Payload["initiator"].(string),
eventPayload.Payload["tokenIn"].(string),
eventPayload.Payload["amountIn"].(string),
)
if err != nil {
return err
}
return nil
}
func loadQueries(queriesPath string) (*queries, error) {
parsedQueries, err := goyesql.ParseFile(queriesPath)
if err != nil {
return nil, err
}
loadedQueries := &queries{}
if err := goyesql.ScanToStruct(loadedQueries, parsedQueries, nil); err != nil {
return nil, fmt.Errorf("failed to scan queries %v", err)
}
return loadedQueries, nil
}
func runMigrations(ctx context.Context, dbPool *pgxpool.Pool, migrationsPath string) error {
ctx, cancel := context.WithTimeout(ctx, migratorTimeout)
defer cancel()
conn, err := dbPool.Acquire(ctx)
if err != nil {
return err
}
defer conn.Release()
migrator, err := migrate.NewMigrator(ctx, conn.Conn(), "schema_version")
if err != nil {
return err
}
if err := migrator.LoadMigrations(os.DirFS(migrationsPath)); err != nil {
return err
}
if err := migrator.Migrate(ctx); err != nil {
return err
}
return nil
}

16
internal/store/store.go Normal file
View File

@@ -0,0 +1,16 @@
package store
import (
"context"
"github.com/grassrootseconomics/celo-indexer/internal/event"
)
type (
Store interface {
InsertTokenTransfer(context.Context, event.Event) error
InsertTokenMint(context.Context, event.Event) error
InsertPoolSwap(context.Context, event.Event) error
InsertPoolDeposit(context.Context, event.Event) error
}
)

134
internal/sub/jetstream.go Normal file
View File

@@ -0,0 +1,134 @@
package sub
import (
"context"
"encoding/json"
"errors"
"log/slog"
"github.com/grassrootseconomics/celo-indexer/internal/event"
"github.com/grassrootseconomics/celo-indexer/internal/store"
"github.com/nats-io/nats.go"
)
const (
durableId = "celo-indexer-6"
pullStream = "TRACKER"
pullSubject = "TRACKER.*"
)
type (
JetStreamOpts struct {
Logg *slog.Logger
Endpoint string
Store store.Store
}
JetStreamSub struct {
natsConn *nats.Conn
jsCtx nats.JetStreamContext
store store.Store
logg *slog.Logger
}
)
func NewJetStreamSub(o JetStreamOpts) (Sub, error) {
natsConn, err := nats.Connect(o.Endpoint)
if err != nil {
return nil, err
}
js, err := natsConn.JetStream()
if err != nil {
return nil, err
}
o.Logg.Info("successfully connected to NATS server")
_, err = js.AddConsumer(pullStream, &nats.ConsumerConfig{
Durable: durableId,
AckPolicy: nats.AckExplicitPolicy,
FilterSubject: pullSubject,
})
if err != nil {
return nil, err
}
return &JetStreamSub{
natsConn: natsConn,
jsCtx: js,
store: o.Store,
logg: o.Logg,
}, nil
}
func (s *JetStreamSub) Close() {
if s.natsConn != nil {
s.natsConn.Close()
}
}
func (s *JetStreamSub) Process() error {
subOpts := []nats.SubOpt{
nats.ManualAck(),
nats.Bind(pullStream, durableId),
}
natsSub, err := s.jsCtx.PullSubscribe(pullSubject, durableId, subOpts...)
if err != nil {
return err
}
for {
events, err := natsSub.Fetch(1)
if err != nil {
if errors.Is(err, nats.ErrTimeout) {
continue
} else if errors.Is(err, nats.ErrConnectionClosed) {
return nil
} else {
return err
}
}
if len(events) > 0 {
msg := events[0]
if err := s.processEventHandler(context.Background(), msg); err != nil {
s.logg.Error("error processing nats message", "error", err)
msg.Nak()
} else {
msg.Ack()
}
}
}
}
func (s *JetStreamSub) processEventHandler(ctx context.Context, msg *nats.Msg) error {
var (
chainEvent event.Event
)
if err := json.Unmarshal(msg.Data, &chainEvent); err != nil {
return err
}
switch msg.Subject {
case "TRACKER.TOKEN_TRANSFER":
if err := s.store.InsertTokenTransfer(ctx, chainEvent); err != nil {
return err
}
case "TRACKER.TOKEN_MINT":
if err := s.store.InsertTokenMint(ctx, chainEvent); err != nil {
return err
}
case "TRACKER.POOL_SWAP":
if err := s.store.InsertPoolSwap(ctx, chainEvent); err != nil {
return err
}
case "TRACKER.POOL_DEPOSIT":
if err := s.store.InsertPoolDeposit(ctx, chainEvent); err != nil {
return err
}
}
return nil
}

8
internal/sub/sub.go Normal file
View File

@@ -0,0 +1,8 @@
package sub
type (
Sub interface {
Process() error
Close()
}
)