Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(indexer) API Observability for Bridge Supplies #7680

Merged
merged 5 commits into from
Nov 8, 2023
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
4 changes: 4 additions & 0 deletions indexer/api-ts/generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,7 @@ export interface WithdrawalResponse {
hasNextPage: boolean;
items: WithdrawalItem[];
}
export interface BridgeSupplyView {
l1DepositSum: number /* float64 */;
l2WithdrawalSum: number /* float64 */;
}
5 changes: 3 additions & 2 deletions indexer/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ const (
HealthPath = "/healthz"
DepositsPath = "/api/v0/deposits/"
WithdrawalsPath = "/api/v0/withdrawals/"

SupplyPath = "/api/v0/supply"
)

// Api ... Indexer API struct
Expand Down Expand Up @@ -140,15 +142,14 @@ func (a *APIService) initRouter(apiConfig config.ServerConfig) {

promRecorder := metrics.NewPromHTTPRecorder(a.metricsRegistry, MetricsNamespace)

// (2) Inject routing middleware
apiRouter.Use(chiMetricsMiddleware(promRecorder))
apiRouter.Use(middleware.Timeout(time.Duration(apiConfig.WriteTimeout) * time.Second))
apiRouter.Use(middleware.Recoverer)
apiRouter.Use(middleware.Heartbeat(HealthPath))

// (3) Set GET routes
apiRouter.Get(fmt.Sprintf(DepositsPath+addressParam, ethereumAddressRegex), h.L1DepositsHandler)
apiRouter.Get(fmt.Sprintf(WithdrawalsPath+addressParam, ethereumAddressRegex), h.L2WithdrawalsHandler)
apiRouter.Get(SupplyPath, h.SupplyView)
a.router = apiRouter
}

Expand Down
8 changes: 8 additions & 0 deletions indexer/api/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,14 @@ func (mbv *MockBridgeTransfersView) L2BridgeWithdrawalsByAddress(address common.
},
}, nil
}

func (mbv *MockBridgeTransfersView) L1BridgeDepositSum() (float64, error) {
return 69, nil
}
func (mbv *MockBridgeTransfersView) L2BridgeWithdrawalSum() (float64, error) {
return 420, nil
}

func TestHealthz(t *testing.T) {
logger := testlog.Logger(t, log.LvlInfo)
cfg := &Config{
Expand Down
5 changes: 5 additions & 0 deletions indexer/api/models/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ type WithdrawalResponse struct {
Items []WithdrawalItem `json:"items"`
}

type BridgeSupplyView struct {
L1DepositSum float64 `json:"l1DepositSum"`
L2WithdrawalSum float64 `json:"l2WithdrawalSum"`
}

// FIXME make a pure function that returns a struct instead of newWithdrawalResponse
// newWithdrawalResponse ... Converts a database.L2BridgeWithdrawalsResponse to an api.WithdrawalResponse
func CreateWithdrawalResponse(withdrawals *database.L2BridgeWithdrawalsResponse) WithdrawalResponse {
Expand Down
35 changes: 35 additions & 0 deletions indexer/api/routes/supply.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package routes

import (
"net/http"

"github.com/ethereum-optimism/optimism/indexer/api/models"
)

// SupplyView ... Handles /api/v0/supply GET requests
func (h Routes) SupplyView(w http.ResponseWriter, r *http.Request) {

depositSum, err := h.view.L1BridgeDepositSum()
if err != nil {
http.Error(w, "internal server error reading deposits", http.StatusInternalServerError)
h.logger.Error("unable to read deposits from DB", "err", err.Error())
return
}

withdrawalSum, err := h.view.L2BridgeWithdrawalSum()
if err != nil {
http.Error(w, "internal server error reading withdrawals", http.StatusInternalServerError)
h.logger.Error("unable to read withdrawals from DB", "err", err.Error())
return
}

view := models.BridgeSupplyView{
L1DepositSum: depositSum,
L2WithdrawalSum: withdrawalSum,
}

err = jsonResponse(w, view, http.StatusOK)
if err != nil {
h.logger.Error("error writing response", "err", err)
}
}
20 changes: 20 additions & 0 deletions indexer/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const (
healthz = "get_health"
deposits = "get_deposits"
withdrawals = "get_withdrawals"
sum = "get_sum"
)

// Option ... Provides configuration through callback injection
Expand Down Expand Up @@ -164,6 +165,25 @@ func (c *Client) GetAllDepositsByAddress(l1Address common.Address) ([]models.Dep

}

// GetSupplyAssessment ... Returns an assessment of the current supply
// on both L1 and L2. This includes the individual sums of
// (L1/L2) deposits and withdrawals
func (c *Client) GetSupplyAssessment() (*models.BridgeSupplyView, error) {
url := c.cfg.BaseURL + api.SupplyPath

resp, err := c.doRecordRequest(sum, url)
if err != nil {
return nil, err
}

var bsv *models.BridgeSupplyView
if err := json.Unmarshal(resp, &bsv); err != nil {
return nil, err
}

return bsv, nil
}

// GetAllWithdrawalsByAddress ... Gets all withdrawals provided a L2 address
func (c *Client) GetAllWithdrawalsByAddress(l2Address common.Address) ([]models.WithdrawalItem, error) {
var withdrawals []models.WithdrawalItem
Expand Down
23 changes: 23 additions & 0 deletions indexer/database/bridge_transfers.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,12 @@ type L2BridgeWithdrawalWithTransactionHashes struct {

type BridgeTransfersView interface {
L1BridgeDeposit(common.Hash) (*L1BridgeDeposit, error)
L1BridgeDepositSum() (float64, error)
L1BridgeDepositWithFilter(BridgeTransfer) (*L1BridgeDeposit, error)
L1BridgeDepositsByAddress(common.Address, string, int) (*L1BridgeDepositsResponse, error)

L2BridgeWithdrawal(common.Hash) (*L2BridgeWithdrawal, error)
L2BridgeWithdrawalSum() (float64, error)
L2BridgeWithdrawalWithFilter(BridgeTransfer) (*L2BridgeWithdrawal, error)
L2BridgeWithdrawalsByAddress(common.Address, string, int) (*L2BridgeWithdrawalsResponse, error)
}
Expand Down Expand Up @@ -136,6 +138,17 @@ type L1BridgeDepositsResponse struct {
HasNextPage bool
}

// L1BridgeDepositSum ... returns the sum of all l1 bridge deposit mints in gwei
func (db *bridgeTransfersDB) L1BridgeDepositSum() (float64, error) {
var sum float64
result := db.gorm.Model(&L1TransactionDeposit{}).Select("sum(amount)").Scan(&sum)
if result.Error != nil {
return 0, result.Error
}

return sum, nil
}

// L1BridgeDepositsByAddress retrieves a list of deposits initiated by the specified address,
// coupled with the L1/L2 transaction hashes that complete the bridge transaction.
func (db *bridgeTransfersDB) L1BridgeDepositsByAddress(address common.Address, cursor string, limit int) (*L1BridgeDepositsResponse, error) {
Expand Down Expand Up @@ -233,6 +246,16 @@ func (db *bridgeTransfersDB) L2BridgeWithdrawal(txWithdrawalHash common.Hash) (*
return &withdrawal, nil
}

func (db *bridgeTransfersDB) L2BridgeWithdrawalSum() (float64, error) {
var sum float64
result := db.gorm.Model(&L2TransactionWithdrawal{}).Select("sum(amount)").Scan(&sum)
if result.Error != nil {
return 0, result.Error
}

return sum, nil
}

// L2BridgeWithdrawalWithFilter queries for a bridge withdrawal with set fields in the `BridgeTransfer` filter
func (db *bridgeTransfersDB) L2BridgeWithdrawalWithFilter(filter BridgeTransfer) (*L2BridgeWithdrawal, error) {
var withdrawal L2BridgeWithdrawal
Expand Down
26 changes: 25 additions & 1 deletion indexer/e2e_tests/bridge_transfers_e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"testing"
"time"

"github.com/ethereum-optimism/optimism/indexer/bigint"
e2etest_utils "github.com/ethereum-optimism/optimism/indexer/e2e_tests/utils"
op_e2e "github.com/ethereum-optimism/optimism/op-e2e"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/transactions"
Expand Down Expand Up @@ -447,7 +448,7 @@ func TestE2EBridgeTransfersCursoredWithdrawals(t *testing.T) {
}
}

func TestClientGetWithdrawals(t *testing.T) {
func TestClientBridgeFunctions(t *testing.T) {
testSuite := createE2ETestSuite(t)

// (1) Generate contract bindings for the L1 and L2 standard bridges
Expand All @@ -459,12 +460,16 @@ func TestClientGetWithdrawals(t *testing.T) {
// (2) Create test actors that will deposit and withdraw using the standard bridge
aliceAddr := testSuite.OpCfg.Secrets.Addresses().Alice
bobAddr := testSuite.OpCfg.Secrets.Addresses().Bob
malAddr := testSuite.OpCfg.Secrets.Addresses().Mallory

type actor struct {
addr common.Address
priv *ecdsa.PrivateKey
}

mintSum := bigint.Zero
withdrawSum := bigint.Zero

actors := []actor{
{
addr: aliceAddr,
Expand All @@ -474,6 +479,10 @@ func TestClientGetWithdrawals(t *testing.T) {
addr: bobAddr,
priv: testSuite.OpCfg.Secrets.Bob,
},
{
addr: malAddr,
priv: testSuite.OpCfg.Secrets.Mallory,
},
}

// (3) Iterate over each actor and deposit / withdraw
Expand All @@ -491,6 +500,8 @@ func TestClientGetWithdrawals(t *testing.T) {
_, err = wait.ForReceiptOK(context.Background(), testSuite.L1Client, depositTx.Hash())
require.NoError(t, err)

mintSum = new(big.Int).Add(mintSum, depositTx.Value())

// (3.b) Initiate withdrawal transaction via L2ToL1MessagePasser contract
l2ToL1MessagePasserWithdrawTx, err := l2ToL1MessagePasser.Receive(l2Opts)
require.NoError(t, err)
Expand All @@ -503,6 +514,8 @@ func TestClientGetWithdrawals(t *testing.T) {
return l2Header != nil && l2Header.Number.Uint64() >= l2ToL1WithdrawReceipt.BlockNumber.Uint64(), nil
}))

withdrawSum = new(big.Int).Add(withdrawSum, l2ToL1MessagePasserWithdrawTx.Value())

// (3.d) Ensure that withdrawal and deposit txs are retrievable via API
deposits, err := testSuite.Client.GetAllDepositsByAddress(actor.addr)
require.NoError(t, err)
Expand All @@ -513,6 +526,17 @@ func TestClientGetWithdrawals(t *testing.T) {
require.NoError(t, err)
require.Len(t, withdrawals, 1)
require.Equal(t, l2ToL1MessagePasserWithdrawTx.Hash().String(), withdrawals[0].TransactionHash)

}

// (4) Ensure that supply assessment is correct
assessment, err := testSuite.Client.GetSupplyAssessment()
require.NoError(t, err)

mintFloat, _ := mintSum.Float64()
require.Equal(t, mintFloat, assessment.L1DepositSum)

withdrawFloat, _ := withdrawSum.Float64()
require.Equal(t, withdrawFloat, assessment.L2WithdrawalSum)

}
4 changes: 2 additions & 2 deletions indexer/migrations/20230523_create_schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,8 @@ CREATE TABLE IF NOT EXISTS l1_transaction_deposits (

-- transaction data. NOTE: `to_address` is the recipient of funds transferred in value field of the
-- L2 deposit transaction and not the amount minted on L1 from the source address. Hence the `amount`
-- column in this table does NOT indiciate the amount transferred to the recipient but instead funds
-- bridged from L1 into `from_address`.
-- column in this table does NOT indicate the amount transferred to the recipient but instead funds
-- bridged from L1 by the `from_address`.
from_address VARCHAR NOT NULL,
to_address VARCHAR NOT NULL,

Expand Down