init: wip indexer
This commit is contained in:
36
internal/event/event.go
Normal file
36
internal/event/event.go
Normal 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
287
internal/store/pg.go
Normal 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
16
internal/store/store.go
Normal 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
134
internal/sub/jetstream.go
Normal 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
8
internal/sub/sub.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package sub
|
||||
|
||||
type (
|
||||
Sub interface {
|
||||
Process() error
|
||||
Close()
|
||||
}
|
||||
)
|
||||
Reference in New Issue
Block a user