diff --git a/indexer/db/db.go b/indexer/db/db.go index cdb4bbf0ace35..288d463834a6b 100644 --- a/indexer/db/db.go +++ b/indexer/db/db.go @@ -197,6 +197,12 @@ func (d *Database) AddIndexedL1Block(block *IndexedL1Block) error { VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) ` + + const updateWithdrawalStatement = ` + UPDATE withdrawals SET (br_withdrawal_finalized_tx_hash, br_withdrawal_finalized_log_index, br_withdrawal_finalized_success) = ($1, $2, $3) + WHERE br_withdrawal_hash = $4 + ` + return txn(d.db, func(tx *sql.Tx) error { _, err := tx.Exec( insertBlockStatement, @@ -209,26 +215,39 @@ func (d *Database) AddIndexedL1Block(block *IndexedL1Block) error { return err } - if len(block.Deposits) == 0 { - return nil + if len(block.Deposits) > 0 { + for _, deposit := range block.Deposits { + _, err = tx.Exec( + insertDepositStatement, + NewGUID(), + deposit.FromAddress.String(), + deposit.ToAddress.String(), + deposit.L1Token.String(), + deposit.L2Token.String(), + deposit.Amount.String(), + deposit.TxHash.String(), + deposit.LogIndex, + block.Hash.String(), + deposit.Data, + ) + if err != nil { + return err + } + } } - for _, deposit := range block.Deposits { - _, err = tx.Exec( - insertDepositStatement, - NewGUID(), - deposit.FromAddress.String(), - deposit.ToAddress.String(), - deposit.L1Token.String(), - deposit.L2Token.String(), - deposit.Amount.String(), - deposit.TxHash.String(), - deposit.LogIndex, - block.Hash.String(), - deposit.Data, - ) - if err != nil { - return err + if len(block.FinalizedWithdrawals) > 0 { + for _, wd := range block.FinalizedWithdrawals { + _, err = tx.Exec( + updateWithdrawalStatement, + wd.TxHash.String(), + wd.LogIndex, + wd.Success, + wd.WithdrawalHash.String(), + ) + if err != nil { + return err + } } } @@ -459,19 +478,21 @@ func (d *Database) GetWithdrawalBatch(hash common.Hash) (*StateBatchJSON, error) // GetWithdrawalsByAddress returns the list of Withdrawals indexed for the given // address paginated by the given params. -func (d *Database) GetWithdrawalsByAddress(address common.Address, page PaginationParam) (*PaginatedWithdrawals, error) { +func (d *Database) GetWithdrawalsByAddress(address common.Address, page PaginationParam, state FinalizationState) (*PaginatedWithdrawals, error) { selectWithdrawalsStatement := fmt.Sprintf(` SELECT withdrawals.guid, withdrawals.from_address, withdrawals.to_address, withdrawals.amount, withdrawals.tx_hash, withdrawals.data, withdrawals.l1_token, withdrawals.l2_token, l2_tokens.name, l2_tokens.symbol, l2_tokens.decimals, - l2_blocks.number, l2_blocks.timestamp, withdrawals.br_withdrawal_hash + l2_blocks.number, l2_blocks.timestamp, withdrawals.br_withdrawal_hash, + withdrawals.br_withdrawal_finalized_tx_hash, withdrawals.br_withdrawal_finalized_log_index, + withdrawals.br_withdrawal_finalized_success FROM withdrawals INNER JOIN l2_blocks ON withdrawals.block_hash=l2_blocks.hash INNER JOIN l2_tokens ON withdrawals.l2_token=l2_tokens.address WHERE withdrawals.from_address = $1 %s ORDER BY l2_blocks.timestamp LIMIT $2 OFFSET $3; - `, FinalizationStateAny.SQL()) + `, state.SQL()) var withdrawals []WithdrawalJSON err := txn(d.db, func(tx *sql.Tx) error { @@ -485,13 +506,16 @@ func (d *Database) GetWithdrawalsByAddress(address common.Address, page Paginati var withdrawal WithdrawalJSON var l2Token Token var wdHash sql.NullString + var finTxHash sql.NullString + var finLogIndex sql.NullInt32 + var finSuccess sql.NullBool if err := rows.Scan( &withdrawal.GUID, &withdrawal.FromAddress, &withdrawal.ToAddress, &withdrawal.Amount, &withdrawal.TxHash, &withdrawal.Data, &withdrawal.L1Token, &l2Token.Address, &l2Token.Name, &l2Token.Symbol, &l2Token.Decimals, &withdrawal.BlockNumber, &withdrawal.BlockTimestamp, - &wdHash, + &wdHash, &finTxHash, &finLogIndex, &finSuccess, ); err != nil { return err } @@ -499,6 +523,16 @@ func (d *Database) GetWithdrawalsByAddress(address common.Address, page Paginati if wdHash.Valid { withdrawal.BedrockWithdrawalHash = &wdHash.String } + if finTxHash.Valid { + withdrawal.BedrockFinalizedTxHash = &finTxHash.String + } + if finLogIndex.Valid { + idx := int(finLogIndex.Int32) + withdrawal.BedrockFinalizedLogIndex = &idx + } + if finSuccess.Valid { + withdrawal.BedrockFinalizedSuccess = &finSuccess.Bool + } withdrawals = append(withdrawals, withdrawal) } diff --git a/indexer/db/l1block.go b/indexer/db/l1block.go index 839d75010a58b..53b506d6ae005 100644 --- a/indexer/db/l1block.go +++ b/indexer/db/l1block.go @@ -6,11 +6,12 @@ import ( // IndexedL1Block contains the L1 block including the deposits in it. type IndexedL1Block struct { - Hash common.Hash - ParentHash common.Hash - Number uint64 - Timestamp uint64 - Deposits []Deposit + Hash common.Hash + ParentHash common.Hash + Number uint64 + Timestamp uint64 + Deposits []Deposit + FinalizedWithdrawals []FinalizedWithdrawal } // String returns the block hash for the indexed l1 block. diff --git a/indexer/db/sql.go b/indexer/db/sql.go index 667eca841e9e2..21c16af255eb9 100644 --- a/indexer/db/sql.go +++ b/indexer/db/sql.go @@ -125,8 +125,8 @@ CREATE TABLE IF NOT EXISTS airdrops ( const updateWithdrawalsTable = ` ALTER TABLE withdrawals ADD COLUMN IF NOT EXISTS br_withdrawal_hash VARCHAR NULL; ALTER TABLE withdrawals ADD COLUMN IF NOT EXISTS br_withdrawal_finalized_tx_hash VARCHAR NULL; -ALTER TABLE withdrawals ADD COLUMN IF NOT EXISTS br_withdrawal_finalized_log_index BOOLEAN NULL; -ALTER TABLE withdrawals ADD COLUMN IF NOT EXISTS br_withdrawal_success BOOLEAN NULL; +ALTER TABLE withdrawals ADD COLUMN IF NOT EXISTS br_withdrawal_finalized_log_index INTEGER NULL; +ALTER TABLE withdrawals ADD COLUMN IF NOT EXISTS br_withdrawal_finalized_success BOOLEAN NULL; CREATE INDEX IF NOT EXISTS withdrawals_br_withdrawal_hash ON withdrawals(br_withdrawal_hash); ` diff --git a/indexer/db/withdrawal.go b/indexer/db/withdrawal.go index 2595b315f122d..2cc31963ac68b 100644 --- a/indexer/db/withdrawal.go +++ b/indexer/db/withdrawal.go @@ -27,19 +27,22 @@ func (w Withdrawal) String() string { // WithdrawalJSON contains Withdrawal data suitable for JSON serialization. type WithdrawalJSON struct { - GUID string `json:"guid"` - FromAddress string `json:"from"` - ToAddress string `json:"to"` - L1Token string `json:"l1Token"` - L2Token *Token `json:"l2Token"` - Amount string `json:"amount"` - Data []byte `json:"data"` - LogIndex uint64 `json:"logIndex"` - BlockNumber uint64 `json:"blockNumber"` - BlockTimestamp string `json:"blockTimestamp"` - TxHash string `json:"transactionHash"` - Batch *StateBatchJSON `json:"batch"` - BedrockWithdrawalHash *string `json:"bedrockWithdrawalHash"` + GUID string `json:"guid"` + FromAddress string `json:"from"` + ToAddress string `json:"to"` + L1Token string `json:"l1Token"` + L2Token *Token `json:"l2Token"` + Amount string `json:"amount"` + Data []byte `json:"data"` + LogIndex uint64 `json:"logIndex"` + BlockNumber uint64 `json:"blockNumber"` + BlockTimestamp string `json:"blockTimestamp"` + TxHash string `json:"transactionHash"` + Batch *StateBatchJSON `json:"batch"` + BedrockWithdrawalHash *string `json:"bedrockWithdrawalHash"` + BedrockFinalizedTxHash *string `json:"bedrockFinalizedTxHash"` + BedrockFinalizedLogIndex *int `json:"bedrockFinalizedLogIndex"` + BedrockFinalizedSuccess *bool `json:"bedrockFinalizedSuccess"` } type FinalizationState int @@ -64,9 +67,9 @@ func ParseFinalizationState(in string) FinalizationState { func (f FinalizationState) SQL() string { switch f { case FinalizationStateFinalized: - return "AND withdrawals.l1_block_hash IS NOT NULL" + return "AND withdrawals.br_withdrawal_finalized_tx_hash IS NOT NULL" case FinalizationStateUnfinalized: - return "AND withdrawals.l2_block_hash IS NULL" + return "AND withdrawals.br_withdrawal_finalized_tx_hash IS NULL" } return "" diff --git a/indexer/services/addresses.go b/indexer/services/addresses.go new file mode 100644 index 0000000000000..40908b1004364 --- /dev/null +++ b/indexer/services/addresses.go @@ -0,0 +1,105 @@ +package services + +import ( + "github.com/ethereum-optimism/optimism/indexer/bindings/legacy/scc" + "github.com/ethereum-optimism/optimism/op-bindings/bindings" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" +) + +type AddressManager interface { + L1StandardBridge() (common.Address, *bindings.L1StandardBridge) + StateCommitmentChain() (common.Address, *scc.StateCommitmentChain) + OptimismPortal() (common.Address, *bindings.OptimismPortal) +} + +type LegacyAddresses struct { + l1SB *bindings.L1StandardBridge + l1SBAddr common.Address + scc *scc.StateCommitmentChain + sccAddr common.Address +} + +var _ AddressManager = (*LegacyAddresses)(nil) + +func NewLegacyAddresses(client bind.ContractBackend, addrMgrAddr common.Address) (AddressManager, error) { + mgr, err := bindings.NewAddressManager(addrMgrAddr, client) + if err != nil { + return nil, err + } + + l1SBAddr, err := mgr.GetAddress(nil, "Proxy__OVM_L1StandardBridge") + if err != nil { + return nil, err + } + sccAddr, err := mgr.GetAddress(nil, "StateCommitmentChain") + if err != nil { + return nil, err + } + l1SB, err := bindings.NewL1StandardBridge(l1SBAddr, client) + if err != nil { + return nil, err + } + sccContract, err := scc.NewStateCommitmentChain(sccAddr, client) + if err != nil { + return nil, err + } + + return &LegacyAddresses{ + l1SB: l1SB, + l1SBAddr: l1SBAddr, + scc: sccContract, + sccAddr: sccAddr, + }, nil +} + +func (a *LegacyAddresses) L1StandardBridge() (common.Address, *bindings.L1StandardBridge) { + return a.l1SBAddr, a.l1SB +} + +func (a *LegacyAddresses) StateCommitmentChain() (common.Address, *scc.StateCommitmentChain) { + return a.sccAddr, a.scc +} + +func (a *LegacyAddresses) OptimismPortal() (common.Address, *bindings.OptimismPortal) { + panic("OptimismPortal not configured on legacy networks - this is a programmer error") +} + +type BedrockAddresses struct { + l1SB *bindings.L1StandardBridge + l1SBAddr common.Address + portal *bindings.OptimismPortal + portalAddr common.Address +} + +var _ AddressManager = (*BedrockAddresses)(nil) + +func NewBedrockAddresses(client bind.ContractBackend, l1SBAddr, portalAddr common.Address) (AddressManager, error) { + l1SB, err := bindings.NewL1StandardBridge(l1SBAddr, client) + if err != nil { + return nil, err + } + portal, err := bindings.NewOptimismPortal(portalAddr, client) + if err != nil { + return nil, err + } + + return &BedrockAddresses{ + l1SB: l1SB, + l1SBAddr: l1SBAddr, + portal: portal, + portalAddr: portalAddr, + }, nil +} + +func (b *BedrockAddresses) L1StandardBridge() (common.Address, *bindings.L1StandardBridge) { + return b.l1SBAddr, b.l1SB +} + +func (b *BedrockAddresses) StateCommitmentChain() (common.Address, *scc.StateCommitmentChain) { + panic("SCC not configured on legacy networks - this is a programmer error") +} + +func (b *BedrockAddresses) OptimismPortal() (common.Address, *bindings.OptimismPortal) { + return b.portalAddr, b.portal +} diff --git a/indexer/services/l1/bridge/bridge.go b/indexer/services/l1/bridge/bridge.go index 69df64c17a7c4..26dfc6dd025df 100644 --- a/indexer/services/l1/bridge/bridge.go +++ b/indexer/services/l1/bridge/bridge.go @@ -13,8 +13,17 @@ import ( "github.com/ethereum/go-ethereum/common" ) +// DepositsMap is a collection of deposit objects keyed +// on block hashes. type DepositsMap map[common.Hash][]db.Deposit -type WithdrawalsMap map[common.Hash][]db.Withdrawal // Finalizations + +// WithdrawalsMap is a collection of withdrawal objects keyed +// on block hashes. +type WithdrawalsMap map[common.Hash][]db.Withdrawal + +// FinalizedWithdrawalsMap is a collection of finalized withdrawal +// objected keyed on block hashes. +type FinalizedWithdrawalsMap map[common.Hash][]db.FinalizedWithdrawal type Bridge interface { Address() common.Address diff --git a/indexer/services/l1/bridge/portal.go b/indexer/services/l1/bridge/portal.go new file mode 100644 index 0000000000000..6a2c3efb08aa3 --- /dev/null +++ b/indexer/services/l1/bridge/portal.go @@ -0,0 +1,63 @@ +package bridge + +import ( + "context" + + "github.com/ethereum-optimism/optimism/indexer/db" + "github.com/ethereum-optimism/optimism/indexer/services" + "github.com/ethereum-optimism/optimism/op-bindings/bindings" + "github.com/ethereum-optimism/optimism/op-service/backoff" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" +) + +type Portal struct { + address common.Address + contract *bindings.OptimismPortal +} + +func NewPortal(addrs services.AddressManager) *Portal { + address, contract := addrs.OptimismPortal() + + return &Portal{ + address: address, + contract: contract, + } +} + +func (p *Portal) Address() common.Address { + return p.address +} + +func (p *Portal) GetFinalizedWithdrawalsByBlockRange(ctx context.Context, start, end uint64) (FinalizedWithdrawalsMap, error) { + wdsByBlockHash := make(FinalizedWithdrawalsMap) + opts := &bind.FilterOpts{ + Context: ctx, + Start: start, + End: &end, + } + + var iter *bindings.OptimismPortalWithdrawalFinalizedIterator + err := backoff.Do(3, backoff.Exponential(), func() error { + var err error + iter, err = p.contract.FilterWithdrawalFinalized(opts, nil) + return err + }) + if err != nil { + return nil, err + } + + defer iter.Close() + for iter.Next() { + wdsByBlockHash[iter.Event.Raw.BlockHash] = append( + wdsByBlockHash[iter.Event.Raw.BlockHash], db.FinalizedWithdrawal{ + TxHash: iter.Event.Raw.TxHash, + WithdrawalHash: iter.Event.WithdrawalHash, + Success: iter.Event.Success, + LogIndex: iter.Event.Raw.Index, + }, + ) + } + + return wdsByBlockHash, iter.Error() +} diff --git a/indexer/services/l2/service.go b/indexer/services/l2/service.go index 31bfcaa4351ca..2d96444d02028 100644 --- a/indexer/services/l2/service.go +++ b/indexer/services/l2/service.go @@ -385,7 +385,7 @@ func (s *Service) GetWithdrawals(w http.ResponseWriter, r *http.Request) { Offset: uint64(offset), } - withdrawals, err := s.cfg.DB.GetWithdrawalsByAddress(common.HexToAddress(vars["address"]), page) + withdrawals, err := s.cfg.DB.GetWithdrawalsByAddress(common.HexToAddress(vars["address"]), page, db.FinalizationStateAny) if err != nil { server.RespondWithError(w, http.StatusInternalServerError, err.Error()) return diff --git a/indexer/services/query/erc20.go b/indexer/services/query/erc20.go new file mode 100644 index 0000000000000..5b2a793495dbd --- /dev/null +++ b/indexer/services/query/erc20.go @@ -0,0 +1,37 @@ +package query + +import ( + "github.com/ethereum-optimism/optimism/indexer/db" + "github.com/ethereum-optimism/optimism/op-bindings/bindings" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethclient" +) + +func NewERC20(address common.Address, client *ethclient.Client) (*db.Token, error) { + contract, err := bindings.NewERC20(address, client) + if err != nil { + return nil, err + } + + name, err := contract.Name(&bind.CallOpts{}) + if err != nil { + return nil, err + } + + symbol, err := contract.Symbol(&bind.CallOpts{}) + if err != nil { + return nil, err + } + + decimals, err := contract.Decimals(&bind.CallOpts{}) + if err != nil { + return nil, err + } + + return &db.Token{ + Name: name, + Symbol: symbol, + Decimals: decimals, + }, nil +} diff --git a/indexer/services/query/headers.go b/indexer/services/query/headers.go new file mode 100644 index 0000000000000..52c1e7e3e34c8 --- /dev/null +++ b/indexer/services/query/headers.go @@ -0,0 +1,20 @@ +package query + +import ( + "context" + + "github.com/ethereum-optimism/optimism/op-service/backoff" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/ethclient" +) + +// HeaderByNumberWithRetry retries getting headers. +func HeaderByNumberWithRetry(ctx context.Context, client *ethclient.Client) (*types.Header, error) { + var res *types.Header + err := backoff.DoCtx(ctx, 3, backoff.Exponential(), func() error { + var err error + res, err = client.HeaderByNumber(ctx, nil) + return err + }) + return res, err +}