diff --git a/indexer/api-ts/generated.ts b/indexer/api-ts/generated.ts index 957d72fddc07..7f9473cc5534 100644 --- a/indexer/api-ts/generated.ts +++ b/indexer/api-ts/generated.ts @@ -51,3 +51,7 @@ export interface WithdrawalResponse { hasNextPage: boolean; items: WithdrawalItem[]; } +export interface BridgeSupplyView { + l1DepositSum: number /* float64 */; + l2WithdrawalSum: number /* float64 */; +} diff --git a/indexer/api/api.go b/indexer/api/api.go index e844839d55bf..cee666761819 100644 --- a/indexer/api/api.go +++ b/indexer/api/api.go @@ -35,6 +35,8 @@ const ( HealthPath = "/healthz" DepositsPath = "/api/v0/deposits/" WithdrawalsPath = "/api/v0/withdrawals/" + + SupplyPath = "/api/v0/supply" ) // Api ... Indexer API struct @@ -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 } diff --git a/indexer/api/api_test.go b/indexer/api/api_test.go index ba01d858b4de..4bfbc38907af 100644 --- a/indexer/api/api_test.go +++ b/indexer/api/api_test.go @@ -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{ diff --git a/indexer/api/models/models.go b/indexer/api/models/models.go index 1bf86ee26192..b3e0964204bf 100644 --- a/indexer/api/models/models.go +++ b/indexer/api/models/models.go @@ -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 { diff --git a/indexer/api/routes/supply.go b/indexer/api/routes/supply.go new file mode 100644 index 000000000000..7f0ece832234 --- /dev/null +++ b/indexer/api/routes/supply.go @@ -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) + } +} diff --git a/indexer/client/client.go b/indexer/client/client.go index 567794d456bb..6b0d1a7dbff4 100644 --- a/indexer/client/client.go +++ b/indexer/client/client.go @@ -23,6 +23,7 @@ const ( healthz = "get_health" deposits = "get_deposits" withdrawals = "get_withdrawals" + sum = "get_sum" ) // Option ... Provides configuration through callback injection @@ -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 diff --git a/indexer/database/bridge_transfers.go b/indexer/database/bridge_transfers.go index 543b63dbd8e9..6704accf5911 100644 --- a/indexer/database/bridge_transfers.go +++ b/indexer/database/bridge_transfers.go @@ -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) } @@ -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) { @@ -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 diff --git a/indexer/e2e_tests/bridge_transfers_e2e_test.go b/indexer/e2e_tests/bridge_transfers_e2e_test.go index 1c7405b121b5..9f26940bb7f0 100644 --- a/indexer/e2e_tests/bridge_transfers_e2e_test.go +++ b/indexer/e2e_tests/bridge_transfers_e2e_test.go @@ -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" @@ -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 @@ -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, @@ -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 @@ -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) @@ -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) @@ -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) + } diff --git a/indexer/migrations/20230523_create_schema.sql b/indexer/migrations/20230523_create_schema.sql index 19f22dc38c43..3bd7138354e1 100644 --- a/indexer/migrations/20230523_create_schema.sql +++ b/indexer/migrations/20230523_create_schema.sql @@ -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,