Skip to content

Commit a6e07d9

Browse files
authored
Merge pull request #547 from ethpandaops/pk910/el-explorer
show execution data (addresses, transactions & balances)
2 parents 658dedd + 66c136f commit a6e07d9

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

74 files changed

+10677
-253
lines changed

.hack/devnet/run.sh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,9 @@ indexer:
147147
cachePersistenceDelay: 8
148148
disableIndexWriter: false
149149
syncEpochCooldown: 1
150+
executionIndexer:
151+
enabled: true
152+
retention: 4368h
150153
database:
151154
engine: "sqlite"
152155
sqlite:

cmd/dora-explorer/main.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,11 @@ func startFrontend(router *mux.Router) {
198198
router.HandleFunc("/slot/{slotOrHash}", handlers.Slot).Methods("GET")
199199
router.HandleFunc("/slot/{root}/blob/{index}", handlers.SlotBlob).Methods("GET")
200200
router.HandleFunc("/blocks", handlers.Blocks).Methods("GET")
201+
router.HandleFunc("/block/{numberOrHash}", handlers.Block).Methods("GET")
201202
router.HandleFunc("/blobs", handlers.Blobs).Methods("GET")
203+
router.HandleFunc("/address/{address}", handlers.Address).Methods("GET")
204+
router.HandleFunc("/address/{address}/balances", handlers.AddressBalances).Methods("GET")
205+
router.HandleFunc("/tx/{hash}", handlers.Transaction).Methods("GET")
202206
router.HandleFunc("/mev/blocks", handlers.MevBlocks).Methods("GET")
203207

204208
router.HandleFunc("/search", handlers.Search).Methods("GET")

config/default.config.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,10 @@ indexer:
111111
# maximum number of parallel beacon state requests (might cause high memory usage)
112112
maxParallelValidatorSetRequests: 1
113113

114+
executionIndexer:
115+
enabled: false # enable execution data indexing
116+
retention: 4368h # 4368 hours = 6 months
117+
114118
# database configuration
115119
database:
116120
engine: "sqlite" # sqlite / pgsql

db/el_accounts.go

Lines changed: 294 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,294 @@
1+
package db
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
7+
"github.com/ethpandaops/dora/dbtypes"
8+
"github.com/jmoiron/sqlx"
9+
)
10+
11+
func InsertElAccount(account *dbtypes.ElAccount, dbTx *sqlx.Tx) (uint64, error) {
12+
var id uint64
13+
query := EngineQuery(map[dbtypes.DBEngineType]string{
14+
dbtypes.DBEnginePgsql: "INSERT INTO el_accounts (address, funder_id, funded, is_contract, last_nonce, last_block_uid) VALUES ($1, $2, $3, $4, $5, $6) RETURNING id",
15+
dbtypes.DBEngineSqlite: "INSERT INTO el_accounts (address, funder_id, funded, is_contract, last_nonce, last_block_uid) VALUES ($1, $2, $3, $4, $5, $6)",
16+
})
17+
18+
if DbEngine == dbtypes.DBEnginePgsql {
19+
err := dbTx.QueryRow(query, account.Address, account.FunderID, account.Funded, account.IsContract, account.LastNonce, account.LastBlockUid).Scan(&id)
20+
if err != nil {
21+
return 0, err
22+
}
23+
} else {
24+
result, err := dbTx.Exec(query, account.Address, account.FunderID, account.Funded, account.IsContract, account.LastNonce, account.LastBlockUid)
25+
if err != nil {
26+
return 0, err
27+
}
28+
lastID, err := result.LastInsertId()
29+
if err != nil {
30+
return 0, err
31+
}
32+
id = uint64(lastID)
33+
}
34+
return id, nil
35+
}
36+
37+
func GetElAccountByID(id uint64) (*dbtypes.ElAccount, error) {
38+
account := &dbtypes.ElAccount{}
39+
err := ReaderDb.Get(account, "SELECT id, address, funder_id, funded, is_contract, last_nonce, last_block_uid FROM el_accounts WHERE id = $1", id)
40+
if err != nil {
41+
return nil, err
42+
}
43+
return account, nil
44+
}
45+
46+
func GetElAccountByAddress(address []byte) (*dbtypes.ElAccount, error) {
47+
account := &dbtypes.ElAccount{}
48+
err := ReaderDb.Get(account, "SELECT id, address, funder_id, funded, is_contract, last_nonce, last_block_uid FROM el_accounts WHERE address = $1", address)
49+
if err != nil {
50+
return nil, err
51+
}
52+
return account, nil
53+
}
54+
55+
func GetElAccountsByFunder(funderID uint64, offset uint64, limit uint32) ([]*dbtypes.ElAccount, uint64, error) {
56+
var sql strings.Builder
57+
args := []any{funderID}
58+
59+
fmt.Fprint(&sql, `
60+
WITH cte AS (
61+
SELECT id, address, funder_id, funded, is_contract, last_nonce, last_block_uid
62+
FROM el_accounts
63+
WHERE funder_id = $1
64+
)`)
65+
66+
args = append(args, limit)
67+
fmt.Fprintf(&sql, `
68+
SELECT
69+
count(*) AS id,
70+
null AS address,
71+
0 AS funder_id,
72+
0 AS funded,
73+
false AS is_contract,
74+
0 AS last_nonce,
75+
0 AS last_block_uid
76+
FROM cte
77+
UNION ALL SELECT * FROM (
78+
SELECT * FROM cte
79+
ORDER BY funded DESC
80+
LIMIT $%v`, len(args))
81+
82+
if offset > 0 {
83+
args = append(args, offset)
84+
fmt.Fprintf(&sql, " OFFSET $%v", len(args))
85+
}
86+
fmt.Fprint(&sql, ") AS t1")
87+
88+
accounts := []*dbtypes.ElAccount{}
89+
err := ReaderDb.Select(&accounts, sql.String(), args...)
90+
if err != nil {
91+
logger.Errorf("Error while fetching el accounts by funder: %v", err)
92+
return nil, 0, err
93+
}
94+
95+
if len(accounts) == 0 {
96+
return []*dbtypes.ElAccount{}, 0, nil
97+
}
98+
99+
count := accounts[0].ID
100+
return accounts[1:], count, nil
101+
}
102+
103+
func GetElAccountsFiltered(offset uint64, limit uint32, filter *dbtypes.ElAccountFilter) ([]*dbtypes.ElAccount, uint64, error) {
104+
var sql strings.Builder
105+
args := []any{}
106+
107+
fmt.Fprint(&sql, `
108+
WITH cte AS (
109+
SELECT id, address, funder_id, funded, is_contract, last_nonce, last_block_uid
110+
FROM el_accounts
111+
`)
112+
113+
filterOp := "WHERE"
114+
if filter.FunderID > 0 {
115+
args = append(args, filter.FunderID)
116+
fmt.Fprintf(&sql, " %v funder_id = $%v", filterOp, len(args))
117+
filterOp = "AND"
118+
}
119+
if filter.IsContract != nil {
120+
args = append(args, *filter.IsContract)
121+
fmt.Fprintf(&sql, " %v is_contract = $%v", filterOp, len(args))
122+
filterOp = "AND"
123+
}
124+
if filter.MinFunded > 0 {
125+
args = append(args, filter.MinFunded)
126+
fmt.Fprintf(&sql, " %v funded >= $%v", filterOp, len(args))
127+
filterOp = "AND"
128+
}
129+
if filter.MaxFunded > 0 {
130+
args = append(args, filter.MaxFunded)
131+
fmt.Fprintf(&sql, " %v funded <= $%v", filterOp, len(args))
132+
filterOp = "AND"
133+
}
134+
135+
fmt.Fprint(&sql, ")")
136+
137+
args = append(args, limit)
138+
fmt.Fprintf(&sql, `
139+
SELECT
140+
count(*) AS id,
141+
null AS address,
142+
0 AS funder_id,
143+
0 AS funded,
144+
false AS is_contract,
145+
0 AS last_nonce,
146+
0 AS last_block_uid
147+
FROM cte
148+
UNION ALL SELECT * FROM (
149+
SELECT * FROM cte
150+
ORDER BY funded DESC
151+
LIMIT $%v`, len(args))
152+
153+
if offset > 0 {
154+
args = append(args, offset)
155+
fmt.Fprintf(&sql, " OFFSET $%v", len(args))
156+
}
157+
fmt.Fprint(&sql, ") AS t1")
158+
159+
accounts := []*dbtypes.ElAccount{}
160+
err := ReaderDb.Select(&accounts, sql.String(), args...)
161+
if err != nil {
162+
logger.Errorf("Error while fetching filtered el accounts: %v", err)
163+
return nil, 0, err
164+
}
165+
166+
if len(accounts) == 0 {
167+
return []*dbtypes.ElAccount{}, 0, nil
168+
}
169+
170+
count := accounts[0].ID
171+
return accounts[1:], count, nil
172+
}
173+
174+
func UpdateElAccount(account *dbtypes.ElAccount, dbTx *sqlx.Tx) error {
175+
_, err := dbTx.Exec("UPDATE el_accounts SET funder_id = $1, funded = $2, is_contract = $3, last_nonce = $4, last_block_uid = $5 WHERE id = $6",
176+
account.FunderID, account.Funded, account.IsContract, account.LastNonce, account.LastBlockUid, account.ID)
177+
return err
178+
}
179+
180+
// UpdateElAccountsLastNonce batch updates last_nonce and last_block_uid for multiple accounts by ID.
181+
// Uses VALUES clause for efficient batch update - 10-50x faster than individual updates.
182+
func UpdateElAccountsLastNonce(accounts []*dbtypes.ElAccount, dbTx *sqlx.Tx) error {
183+
if len(accounts) == 0 {
184+
return nil
185+
}
186+
187+
// Filter out accounts with zero ID
188+
validAccounts := make([]*dbtypes.ElAccount, 0, len(accounts))
189+
for _, account := range accounts {
190+
if account.ID > 0 {
191+
validAccounts = append(validAccounts, account)
192+
}
193+
}
194+
195+
if len(validAccounts) == 0 {
196+
return nil
197+
}
198+
199+
var sql strings.Builder
200+
args := make([]any, 0, len(validAccounts)*3)
201+
202+
if DbEngine == dbtypes.DBEnginePgsql {
203+
// PostgreSQL: use UPDATE ... FROM VALUES
204+
fmt.Fprint(&sql, `
205+
UPDATE el_accounts AS a SET
206+
last_nonce = v.last_nonce,
207+
last_block_uid = v.last_block_uid
208+
FROM (VALUES `)
209+
210+
for i, account := range validAccounts {
211+
if i > 0 {
212+
fmt.Fprint(&sql, ", ")
213+
}
214+
argIdx := len(args) + 1
215+
fmt.Fprintf(&sql, "($%d, $%d, $%d)", argIdx, argIdx+1, argIdx+2)
216+
args = append(args, account.ID, account.LastNonce, account.LastBlockUid)
217+
}
218+
219+
fmt.Fprint(&sql, `) AS v(id, last_nonce, last_block_uid)
220+
WHERE a.id = v.id`)
221+
} else {
222+
// SQLite: use UPDATE with CASE statements (works in all SQLite versions)
223+
// For SQLite 3.33.0+, could use UPDATE ... FROM VALUES, but CASE is more compatible
224+
if len(validAccounts) == 1 {
225+
// Single update - simple case
226+
args = append(args, validAccounts[0].LastNonce, validAccounts[0].LastBlockUid, validAccounts[0].ID)
227+
fmt.Fprint(&sql, `UPDATE el_accounts SET last_nonce = $1, last_block_uid = $2 WHERE id = $3`)
228+
} else {
229+
// Multiple updates - use CASE statements
230+
fmt.Fprint(&sql, `UPDATE el_accounts SET
231+
last_nonce = CASE id `)
232+
233+
for _, account := range validAccounts {
234+
argIdx := len(args) + 1
235+
fmt.Fprintf(&sql, "WHEN $%d THEN $%d ", argIdx, argIdx+1)
236+
args = append(args, account.ID, account.LastNonce)
237+
}
238+
fmt.Fprint(&sql, "ELSE last_nonce END, last_block_uid = CASE id ")
239+
240+
for _, account := range validAccounts {
241+
argIdx := len(args) + 1
242+
fmt.Fprintf(&sql, "WHEN $%d THEN $%d ", argIdx, argIdx+1)
243+
args = append(args, account.ID, account.LastBlockUid)
244+
}
245+
246+
fmt.Fprint(&sql, "ELSE last_block_uid END WHERE id IN (")
247+
for i, account := range validAccounts {
248+
if i > 0 {
249+
fmt.Fprint(&sql, ", ")
250+
}
251+
argIdx := len(args) + 1
252+
fmt.Fprintf(&sql, "$%d", argIdx)
253+
args = append(args, account.ID)
254+
}
255+
fmt.Fprint(&sql, ")")
256+
}
257+
}
258+
259+
_, err := dbTx.Exec(sql.String(), args...)
260+
return err
261+
}
262+
263+
func DeleteElAccount(id uint64, dbTx *sqlx.Tx) error {
264+
_, err := dbTx.Exec("DELETE FROM el_accounts WHERE id = $1", id)
265+
return err
266+
}
267+
268+
// GetElAccountsByIDs retrieves multiple accounts by their IDs in a single query.
269+
func GetElAccountsByIDs(ids []uint64) ([]*dbtypes.ElAccount, error) {
270+
if len(ids) == 0 {
271+
return []*dbtypes.ElAccount{}, nil
272+
}
273+
274+
var sql strings.Builder
275+
args := make([]any, len(ids))
276+
277+
fmt.Fprint(&sql, "SELECT id, address, funder_id, funded, is_contract, last_nonce, last_block_uid FROM el_accounts WHERE id IN (")
278+
for i, id := range ids {
279+
if i > 0 {
280+
fmt.Fprint(&sql, ", ")
281+
}
282+
fmt.Fprintf(&sql, "$%d", i+1)
283+
args[i] = id
284+
}
285+
fmt.Fprint(&sql, ")")
286+
287+
accounts := []*dbtypes.ElAccount{}
288+
err := ReaderDb.Select(&accounts, sql.String(), args...)
289+
if err != nil {
290+
logger.Errorf("Error while fetching el accounts by IDs: %v", err)
291+
return nil, err
292+
}
293+
return accounts, nil
294+
}

0 commit comments

Comments
 (0)