Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/cuddly-rings-reply.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@eth-optimism/indexer': minor
---

Add airdrops API
13 changes: 13 additions & 0 deletions go/indexer/db/airdrop.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package db

type Airdrop struct {
Address string `json:"address"`
VoterAmount string `json:"voterAmount"`
MultisigSignerAmount string `json:"multisigSignerAmount"`
GitcoinAmount string `json:"gitcoinAmount"`
ActiveBridgedAmount string `json:"activeBridgedAmount"`
OpUserAmount string `json:"opUserAmount"`
OpRepeatUserAmount string `json:"opRepeatUserAmount"`
BonusAmount string `json:"bonusAmount"`
TotalAmount string `json:"totalAmount"`
}
76 changes: 57 additions & 19 deletions go/indexer/db/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package db
import (
"database/sql"
"errors"
"fmt"
"strings"

l2common "github.com/ethereum-optimism/optimism/l2geth/common"
"github.com/ethereum/go-ethereum/common"
Expand All @@ -17,6 +19,31 @@ type Database struct {
config string
}

// NewDatabase returns the database for the given connection string.
func NewDatabase(config string) (*Database, error) {
db, err := sql.Open("postgres", config)
if err != nil {
return nil, err
}

err = db.Ping()
if err != nil {
return nil, err
}

for _, migration := range schema {
_, err = db.Exec(migration)
if err != nil {
return nil, err
}
}

return &Database{
db: db,
config: config,
}, nil
}

// Close closes the database.
// NOTE: "It is rarely necessary to close a DB."
// See: https://pkg.go.dev/database/sql#Open
Expand Down Expand Up @@ -633,27 +660,38 @@ func (d *Database) GetIndexedL1BlockByHash(hash common.Hash) (*IndexedL1Block, e
return block, nil
}

// NewDatabase returns the database for the given connection string.
func NewDatabase(config string) (*Database, error) {
db, err := sql.Open("postgres", config)
if err != nil {
return nil, err
const getAirdropQuery = `
SELECT
address, voter_amount, multisig_signer_amount, gitcoin_amount,
active_bridged_amount, op_user_amount, op_repeat_user_amount,
bonus_amount, total_amount
FROM airdrops
WHERE address = $1
`

func (d *Database) GetAirdrop(address common.Address) (*Airdrop, error) {
row := d.db.QueryRow(getAirdropQuery, strings.ToLower(address.String()))
if row.Err() != nil {
return nil, fmt.Errorf("error getting airdrop: %v", row.Err())
}

err = db.Ping()
if err != nil {
return nil, err
airdrop := new(Airdrop)
err := row.Scan(
&airdrop.Address,
&airdrop.VoterAmount,
&airdrop.MultisigSignerAmount,
&airdrop.GitcoinAmount,
&airdrop.ActiveBridgedAmount,
&airdrop.OpUserAmount,
&airdrop.OpRepeatUserAmount,
&airdrop.BonusAmount,
&airdrop.TotalAmount,
)
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
}

for _, migration := range schema {
_, err = db.Exec(migration)
if err != nil {
return nil, err
}
if err != nil {
return nil, fmt.Errorf("error scanning airdrop: %v", err)
}

return &Database{
db: db,
config: config,
}, nil
return airdrop, nil
}
16 changes: 16 additions & 0 deletions go/indexer/db/sql.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,21 @@ CREATE UNIQUE INDEX IF NOT EXISTS l1_blocks_number ON l1_blocks(number);
CREATE UNIQUE INDEX IF NOT EXISTS l2_blocks_number ON l2_blocks(number);
`

const createAirdropsTable = `
CREATE TABLE IF NOT EXISTS airdrops (
address VARCHAR(42) PRIMARY KEY,
voter_amount VARCHAR NOT NULL DEFAULT '0' CHECK(voter_amount ~ '^\d+$') ,
multisig_signer_amount VARCHAR NOT NULL DEFAULT '0' CHECK(multisig_signer_amount ~ '^\d+$'),
gitcoin_amount VARCHAR NOT NULL DEFAULT '0' CHECK(gitcoin_amount ~ '^\d+$'),
active_bridged_amount VARCHAR NOT NULL DEFAULT '0' CHECK(active_bridged_amount ~ '^\d+$'),
op_user_amount VARCHAR NOT NULL DEFAULT '0' CHECK(op_user_amount ~ '^\d+$'),
op_repeat_user_amount VARCHAR NOT NULL DEFAULT '0' CHECK(op_user_amount ~ '^\d+$'),
op_og_amount VARCHAR NOT NULL DEFAULT '0' CHECK(op_og_amount ~ '^\d+$'),
bonus_amount VARCHAR NOT NULL DEFAULT '0' CHECK(bonus_amount ~ '^\d+$'),
total_amount VARCHAR NOT NULL CHECK(voter_amount ~ '^\d+$')
)
`

var schema = []string{
createL1BlocksTable,
createL2BlocksTable,
Expand All @@ -118,4 +133,5 @@ var schema = []string{
createDepositsTable,
createWithdrawalsTable,
createL1L2NumberIndex,
createAirdropsTable,
}
11 changes: 9 additions & 2 deletions go/indexer/indexer.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import (
"strconv"
"time"

"github.com/ethereum-optimism/optimism/go/indexer/services"

l2rpc "github.com/ethereum-optimism/optimism/l2geth/rpc"

"github.com/ethereum-optimism/optimism/go/indexer/metrics"
Expand Down Expand Up @@ -83,8 +85,10 @@ type Indexer struct {

l1IndexingService *l1.Service
l2IndexingService *l2.Service
airdropService *services.Airdrop

router *mux.Router
router *mux.Router
metrics *metrics.Metrics
}

// NewIndexer initializes the Indexer, gathering any resources
Expand Down Expand Up @@ -201,7 +205,9 @@ func NewIndexer(cfg Config, gitVersion string) (*Indexer, error) {
l2Client: l2Client,
l1IndexingService: l1IndexingService,
l2IndexingService: l2IndexingService,
airdropService: services.NewAirdrop(db, m),
router: mux.NewRouter(),
metrics: m,
}, nil
}

Expand All @@ -216,6 +222,7 @@ func (b *Indexer) Serve() error {
b.router.HandleFunc("/v1/deposits/0x{address:[a-fA-F0-9]{40}}", b.l1IndexingService.GetDeposits).Methods("GET")
b.router.HandleFunc("/v1/withdrawal/0x{hash:[a-fA-F0-9]{64}}", b.l2IndexingService.GetWithdrawalBatch).Methods("GET")
b.router.HandleFunc("/v1/withdrawals/0x{address:[a-fA-F0-9]{40}}", b.l2IndexingService.GetWithdrawals).Methods("GET")
b.router.HandleFunc("/v1/airdrops/0x{address:[a-fA-F0-9]{40}}", b.airdropService.GetAirdrop)
b.router.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
_, err := w.Write([]byte("OK"))
Expand All @@ -224,7 +231,7 @@ func (b *Indexer) Serve() error {
}
})

middleware := server.LoggingMiddleware(log.New("service", "server"))
middleware := server.LoggingMiddleware(b.metrics, log.New("service", "server"))

port := strconv.FormatUint(b.cfg.RESTPort, 10)
addr := fmt.Sprintf("%s:%s", b.cfg.RESTHostname, port)
Expand Down
38 changes: 38 additions & 0 deletions go/indexer/metrics/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package metrics
import (
"fmt"
"net/http"
"strconv"
"time"

l2common "github.com/ethereum-optimism/optimism/l2geth/common"
"github.com/ethereum/go-ethereum/common"
Expand Down Expand Up @@ -32,6 +34,12 @@ type Metrics struct {

CachedTokensCount *prometheus.CounterVec

HTTPRequestsCount prometheus.Counter

HTTPResponsesCount *prometheus.CounterVec

HTTPRequestDurationSecs prometheus.Summary

tokenAddrs map[string]string
}

Expand Down Expand Up @@ -110,6 +118,27 @@ func NewMetrics(monitoredTokens map[string]string) *Metrics {
"chain",
}),

HTTPRequestsCount: promauto.NewCounter(prometheus.CounterOpts{
Name: "http_requests_count",
Help: "How many HTTP requests this instance has seen",
Namespace: metricsNamespace,
}),

HTTPResponsesCount: promauto.NewCounterVec(prometheus.CounterOpts{
Name: "http_responses_count",
Help: "How many HTTP responses this instance has served",
Namespace: metricsNamespace,
}, []string{
"status_code",
}),

HTTPRequestDurationSecs: promauto.NewSummary(prometheus.SummaryOpts{
Name: "http_request_duration_secs",
Help: "How long each HTTP request took",
Namespace: metricsNamespace,
Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.95: 0.005, 0.99: 0.001},
}),

tokenAddrs: mts,
}
}
Expand Down Expand Up @@ -176,6 +205,15 @@ func (m *Metrics) IncL2CachedTokensCount() {
m.CachedTokensCount.WithLabelValues("l2").Inc()
}

func (m *Metrics) RecordHTTPRequest() {
m.HTTPRequestsCount.Inc()
}

func (m *Metrics) RecordHTTPResponse(code int, dur time.Duration) {
m.HTTPResponsesCount.WithLabelValues(strconv.Itoa(code)).Inc()
m.HTTPRequestDurationSecs.Observe(float64(dur) / float64(time.Second))
}

func (m *Metrics) Serve(hostname string, port uint64) (*http.Server, error) {
mux := http.NewServeMux()
mux.Handle("/metrics", promhttp.Handler())
Expand Down
9 changes: 7 additions & 2 deletions go/indexer/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
"runtime/debug"
"time"

"github.com/ethereum-optimism/optimism/go/indexer/metrics"

"github.com/ethereum/go-ethereum/log"
)

Expand Down Expand Up @@ -50,7 +52,7 @@ func (rw *responseWriter) WriteHeader(code int) {
}

// LoggingMiddleware logs the incoming HTTP request & its duration.
func LoggingMiddleware(logger log.Logger) func(http.Handler) http.Handler {
func LoggingMiddleware(metrics *metrics.Metrics, logger log.Logger) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
defer func() {
Expand All @@ -64,16 +66,19 @@ func LoggingMiddleware(logger log.Logger) func(http.Handler) http.Handler {
}
}()

metrics.RecordHTTPRequest()
start := time.Now()
wrapped := wrapResponseWriter(w)
next.ServeHTTP(wrapped, r)
dur := time.Since(start)
logger.Info(
"served request",
"status", wrapped.status,
"method", r.Method,
"path", r.URL.EscapedPath(),
"duration", time.Since(start),
"duration", dur,
)
metrics.RecordHTTPResponse(wrapped.status, dur)
}

return http.HandlerFunc(fn)
Expand Down
44 changes: 44 additions & 0 deletions go/indexer/services/airdrop.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package services

import (
"net/http"

"github.com/ethereum-optimism/optimism/go/indexer/db"
"github.com/ethereum-optimism/optimism/go/indexer/metrics"
"github.com/ethereum-optimism/optimism/go/indexer/server"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log"
"github.com/gorilla/mux"
)

var airdropLogger = log.New("service", "airdrop")

type Airdrop struct {
db *db.Database
metrics *metrics.Metrics
}

func NewAirdrop(db *db.Database, metrics *metrics.Metrics) *Airdrop {
return &Airdrop{
db: db,
metrics: metrics,
}
}

func (a *Airdrop) GetAirdrop(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
address := vars["address"]
airdrop, err := a.db.GetAirdrop(common.HexToAddress(address))
if err != nil {
airdropLogger.Error("db error getting airdrop", "err", err)
server.RespondWithError(w, http.StatusInternalServerError, "database error")
return
}

if airdrop == nil {
server.RespondWithError(w, http.StatusNotFound, "airdrop not found")
return
}

server.RespondWithJSON(w, http.StatusOK, airdrop)
}