diff --git a/tools/analysis-check/cmd/analysis-check/main.go b/tools/analysis-check/cmd/analysis-check/main.go new file mode 100644 index 00000000..0e1daca8 --- /dev/null +++ b/tools/analysis-check/cmd/analysis-check/main.go @@ -0,0 +1,170 @@ +package main + +import ( + "context" + "fmt" + "os" + "os/signal" + "syscall" + "time" + + "github.com/okx/xlayer-toolkit/tools/analysis-check/internal/analyzer" + "github.com/okx/xlayer-toolkit/tools/analysis-check/internal/rpc" + "github.com/urfave/cli/v2" +) + +var ( + rpcURLFlag = &cli.StringFlag{ + Name: "rpc-url", + Usage: "RPC endpoint URL", + Required: true, + } + blockFlag = &cli.StringFlag{ + Name: "block", + Usage: "Block number or hash to analyze (if not set, follows latest blocks)", + } + innerTxFlag = &cli.StringFlag{ + Name: "innertx", + Usage: "Internal transaction fetch mode: 'block' or 'tx'", + Value: "block", + } + batchSizeFlag = &cli.IntFlag{ + Name: "batch-size", + Usage: "Batch size for RPC requests", + Value: 200, + } + pollIntervalFlag = &cli.DurationFlag{ + Name: "poll-interval", + Usage: "Polling interval for latest block mode", + Value: time.Second, + } +) + +func main() { + app := &cli.App{ + Name: "analysis-check", + Usage: "Simulate block analysis by fetching block data, receipts, internal transactions, and account states", + Flags: []cli.Flag{ + rpcURLFlag, + blockFlag, + innerTxFlag, + batchSizeFlag, + pollIntervalFlag, + }, + Action: run, + } + + if err := app.Run(os.Args); err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + os.Exit(1) + } +} + +func run(c *cli.Context) error { + rpcURL := c.String("rpc-url") + blockArg := c.String("block") + innerTxMode := c.String("innertx") + batchSize := c.Int("batch-size") + pollInterval := c.Duration("poll-interval") + + if innerTxMode != "block" && innerTxMode != "tx" { + return fmt.Errorf("invalid innertx mode: %s (must be 'block' or 'tx')", innerTxMode) + } + + client, err := rpc.NewClient(rpcURL) + if err != nil { + return fmt.Errorf("create rpc client: %w", err) + } + defer client.Close() + + a := analyzer.New(client, analyzer.Config{ + InnerTxMode: innerTxMode, + BatchSize: batchSize, + }) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) + go func() { + <-sigCh + fmt.Println("\nShutting down...") + cancel() + }() + + if blockArg != "" { + return runSingleBlock(ctx, a, blockArg) + } + return runLoop(ctx, client, a, pollInterval) +} + +func runSingleBlock(ctx context.Context, a *analyzer.Analyzer, blockArg string) error { + num, hash, isHash, err := rpc.ParseBlockIdentifier(blockArg) + if err != nil { + return err + } + + var result *analyzer.Result + if isHash { + result, err = a.AnalyzeBlockByHash(ctx, hash) + } else { + result, err = a.AnalyzeBlockByNumber(ctx, num) + } + if err != nil { + return fmt.Errorf("analyze block: %w", err) + } + + printResult(result) + return nil +} + +func runLoop(ctx context.Context, client *rpc.Client, a *analyzer.Analyzer, pollInterval time.Duration) error { + var lastProcessed uint64 + + ticker := time.NewTicker(pollInterval) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return nil + case <-ticker.C: + latestNum, err := client.GetBlockNumber(ctx) + if err != nil { + fmt.Fprintf(os.Stderr, "get block number: %v\n", err) + continue + } + + if latestNum <= lastProcessed { + continue + } + + for blockNum := lastProcessed + 1; blockNum <= latestNum; blockNum++ { + if lastProcessed == 0 { + blockNum = latestNum + } + + select { + case <-ctx.Done(): + return nil + default: + } + + result, err := a.AnalyzeBlockByNumber(ctx, blockNum) + if err != nil { + fmt.Fprintf(os.Stderr, "analyze block %d: %v\n", blockNum, err) + continue + } + + printResult(result) + lastProcessed = blockNum + } + } + } +} + +func printResult(r *analyzer.Result) { + fmt.Printf("block=%d, hash=%s, txs=%d, accounts=%d, tokens=%d, innerTxs=%d, analysis elapse: %d ms\n", + r.BlockNumber, r.BlockHash.Hex(), r.TxCount, r.AccountCount, r.TokenCount, r.InnerTxCount, r.ElapsedMs) +} diff --git a/tools/analysis-check/go.mod b/tools/analysis-check/go.mod new file mode 100644 index 00000000..b28c16a6 --- /dev/null +++ b/tools/analysis-check/go.mod @@ -0,0 +1,40 @@ +module github.com/okx/xlayer-toolkit/tools/analysis-check + +go 1.23.0 + +require ( + github.com/ethereum/go-ethereum v1.14.12 + github.com/urfave/cli/v2 v2.27.5 +) + +require ( + github.com/BurntSushi/toml v1.4.0 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/StackExchange/wmi v1.2.1 // indirect + github.com/bits-and-blooms/bitset v1.20.0 // indirect + github.com/consensys/gnark-crypto v0.18.0 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect + github.com/crate-crypto/go-eth-kzg v1.3.0 // indirect + github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a // indirect + github.com/deckarep/golang-set/v2 v2.6.0 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect + github.com/ethereum/c-kzg-4844/v2 v2.1.5 // indirect + github.com/ethereum/go-verkle v0.2.2 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/gorilla/websocket v1.4.2 // indirect + github.com/holiman/uint256 v1.3.2 // indirect + github.com/klauspost/compress v1.17.11 // indirect + github.com/naoina/go-stringutil v0.1.0 // indirect + github.com/naoina/toml v0.1.2-0.20170918210437-9fafd6967416 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect + github.com/supranational/blst v0.3.15 // indirect + github.com/tklauser/go-sysconf v0.3.12 // indirect + github.com/tklauser/numcpus v0.6.1 // indirect + github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect + golang.org/x/crypto v0.36.0 // indirect + golang.org/x/sync v0.12.0 // indirect + golang.org/x/sys v0.31.0 // indirect +) + +replace github.com/ethereum/go-ethereum => github.com/okx/op-geth v0.0.10 diff --git a/tools/analysis-check/go.sum b/tools/analysis-check/go.sum new file mode 100644 index 00000000..52a1b163 --- /dev/null +++ b/tools/analysis-check/go.sum @@ -0,0 +1,195 @@ +github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= +github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/DataDog/zstd v1.5.6-0.20230824185856-869dae002e5e h1:ZIWapoIRN1VqT8GR8jAwb1Ie9GyehWjVcGh32Y2MznE= +github.com/DataDog/zstd v1.5.6-0.20230824185856-869dae002e5e/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA= +github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8= +github.com/VictoriaMetrics/fastcache v1.12.2 h1:N0y9ASrJ0F6h0QaC3o6uJb3NIZ9VKLjCM7NQbSmF7WI= +github.com/VictoriaMetrics/fastcache v1.12.2/go.mod h1:AmC+Nzz1+3G2eCPapF6UcsnkThDcMsQicp4xDukwJYI= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bits-and-blooms/bitset v1.20.0 h1:2F+rfL86jE2d/bmw7OhqUg2Sj/1rURkBn3MdfoPyRVU= +github.com/bits-and-blooms/bitset v1.20.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cockroachdb/errors v1.11.3 h1:5bA+k2Y6r+oz/6Z/RFlNeVCesGARKuC6YymtcDrbC/I= +github.com/cockroachdb/errors v1.11.3/go.mod h1:m4UIW4CDjx+R5cybPsNrRbreomiFqt8o1h1wUVazSd8= +github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce h1:giXvy4KSc/6g/esnpM7Geqxka4WSqI1SZc7sMJFd3y4= +github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce/go.mod h1:9/y3cnZ5GKakj/H4y9r9GTjCvAFta7KLgSHPJJYc52M= +github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE= +github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs= +github.com/cockroachdb/pebble v1.1.5 h1:5AAWCBWbat0uE0blr8qzufZP5tBjkRyy/jWe1QWLnvw= +github.com/cockroachdb/pebble v1.1.5/go.mod h1:17wO9el1YEigxkP/YtV8NtCivQDgoCyBg5c4VR/eOWo= +github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwPJ30= +github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= +github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo= +github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ= +github.com/consensys/gnark-crypto v0.18.0 h1:vIye/FqI50VeAr0B3dx+YjeIvmc3LWz4yEfbWBpTUf0= +github.com/consensys/gnark-crypto v0.18.0/go.mod h1:L3mXGFTe1ZN+RSJ+CLjUt9x7PNdx8ubaYfDROyp2Z8c= +github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= +github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/crate-crypto/go-eth-kzg v1.3.0 h1:05GrhASN9kDAidaFJOda6A4BEvgvuXbazXg/0E3OOdI= +github.com/crate-crypto/go-eth-kzg v1.3.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI= +github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a h1:W8mUrRp6NOVl3J+MYp5kPMoUZPp7aOYHtaua31lwRHg= +github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a/go.mod h1:sTwzHBvIzm2RfVCGNEBZgRyjwK40bVoun3ZnGOCafNM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dchest/siphash v1.2.3 h1:QXwFc8cFOR2dSa/gE6o/HokBMWtLUaNDVd+22aKHeEA= +github.com/dchest/siphash v1.2.3/go.mod h1:0NvQU092bT0ipiFN++/rXm69QG9tVxLAlQHIXMPAkHc= +github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM= +github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= +github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= +github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= +github.com/emicklei/dot v1.6.2 h1:08GN+DD79cy/tzN6uLCT84+2Wk9u+wvqP+Hkx/dIR8A= +github.com/emicklei/dot v1.6.2/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s= +github.com/ethereum/c-kzg-4844/v2 v2.1.5 h1:aVtoLK5xwJ6c5RiqO8g8ptJ5KU+2Hdquf6G3aXiHh5s= +github.com/ethereum/c-kzg-4844/v2 v2.1.5/go.mod h1:u59hRTTah4Co6i9fDWtiCjTrblJv0UwsqZKCc0GfgUs= +github.com/ethereum/go-verkle v0.2.2 h1:I2W0WjnrFUIzzVPwm8ykY+7pL2d4VhlsePn4j7cnFk8= +github.com/ethereum/go-verkle v0.2.2/go.mod h1:M3b90YRnzqKyyzBEWJGqj8Qff4IDeXnzFw0P9bFw3uk= +github.com/ferranbt/fastssz v0.1.4 h1:OCDB+dYDEQDvAgtAGnTSidK1Pe2tW3nFV40XyMkTeDY= +github.com/ferranbt/fastssz v0.1.4/go.mod h1:Ea3+oeoRGGLGm5shYAeDgu6PGUlcvQhE2fILyD9+tGg= +github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps= +github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY= +github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= +github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= +github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb h1:PBC98N2aIaM3XXiurYmW7fx4GZkL8feAMVq7nEjURHk= +github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hashicorp/go-bexpr v0.1.10 h1:9kuI5PFotCboP3dkDYFr/wi0gg0QVbSNz5oFRpxn4uE= +github.com/hashicorp/go-bexpr v0.1.10/go.mod h1:oxlubA2vC/gFVfX1A6JGp7ls7uCDlfJn732ehYYg+g0= +github.com/holiman/billy v0.0.0-20250707135307-f2f9b9aae7db h1:IZUYC/xb3giYwBLMnr8d0TGTzPKFGNTCGgGLoyeX330= +github.com/holiman/billy v0.0.0-20250707135307-f2f9b9aae7db/go.mod h1:xTEYN9KCHxuYHs+NmrmzFcnvHMzLLNiGFafCb1n3Mfg= +github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao= +github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA= +github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA= +github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= +github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= +github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= +github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= +github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= +github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= +github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzWu4= +github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c= +github.com/linxGnu/grocksdb v1.10.2 h1:y0dXsWYULY15/BZMcwAZzLd13ZuyA470vyoNzWwmqG0= +github.com/linxGnu/grocksdb v1.10.2/go.mod h1:C3CNe9UYc9hlEM2pC82AqiGS3LRW537u9LFV4wIZuHk= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g= +github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= +github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/pointerstructure v1.2.0 h1:O+i9nHnXS3l/9Wu7r4NrEdwA2VFTicjUEN1uBnDo34A= +github.com/mitchellh/pointerstructure v1.2.0/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4= +github.com/naoina/go-stringutil v0.1.0 h1:rCUeRUHjBjGTSHl0VC00jUPLz8/F9dDzYI70Hzifhks= +github.com/naoina/go-stringutil v0.1.0/go.mod h1:XJ2SJL9jCtBh+P9q5btrd/Ylo8XwT/h1USek5+NqSA0= +github.com/naoina/toml v0.1.2-0.20170918210437-9fafd6967416 h1:shk/vn9oCoOTmwcouEdwIeOtOGA/ELRUw/GwvxwfT+0= +github.com/naoina/toml v0.1.2-0.20170918210437-9fafd6967416/go.mod h1:NBIhNtsFMo3G2szEBne+bO4gS192HuIYRqfvOWb4i1E= +github.com/okx/op-geth v0.0.10 h1:LyHuCYnu0kntDJnkakCv4nxm264t53C/wHYCfUCgaUU= +github.com/okx/op-geth v0.0.10/go.mod h1:wbOONFIarNosj3FNhtct7r1Za0Oe8Ts9pjJJvdA5ifQ= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/pierrec/lz4/v4 v4.1.15 h1:MO0/ucJhngq7299dKLwIMtgTfbkoSPF6AoMYDd8Q4q0= +github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8= +github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= +github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= +github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= +github.com/pion/stun/v2 v2.0.0 h1:A5+wXKLAypxQri59+tmQKVs7+l6mMM+3d+eER9ifRU0= +github.com/pion/stun/v2 v2.0.0/go.mod h1:22qRSh08fSEttYUmJZGlriq9+03jtVmXNODgLccj8GQ= +github.com/pion/transport/v2 v2.2.1 h1:7qYnCBlpgSJNYMbLCKuSY9KbQdBFoETvPNETv0y4N7c= +github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g= +github.com/pion/transport/v3 v3.0.1 h1:gDTlPJwROfSfz6QfSi0ZmeCSkFcnWWiiR9ES0ouANiM= +github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.15.0 h1:5fCgGYogn0hFdhyhLbw7hEsWxufKtY9klyvdNfFlFhM= +github.com/prometheus/client_golang v1.15.0/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk= +github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= +github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= +github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM= +github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= +github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= +github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= +github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/segmentio/kafka-go v0.4.47 h1:IqziR4pA3vrZq7YdRxaT3w1/5fvIH5qpCwstUanQQB0= +github.com/segmentio/kafka-go v0.4.47/go.mod h1:HjF6XbOKh0Pjlkr5GVZxt6CsjjwnmhVOfURM5KMd8qg= +github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible h1:Bn1aCHHRnjv4Bl16T8rcaFjYSrGrIZvpiGO6P3Q4GpU= +github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/supranational/blst v0.3.15 h1:rd9viN6tfARE5wv3KZJ9H8e1cg0jXW8syFCcsbHa76o= +github.com/supranational/blst v0.3.15/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= +github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= +github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= +github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME= +golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= +golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= +golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= +golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/tools/analysis-check/internal/analyzer/analyzer.go b/tools/analysis-check/internal/analyzer/analyzer.go new file mode 100644 index 00000000..b11c7349 --- /dev/null +++ b/tools/analysis-check/internal/analyzer/analyzer.go @@ -0,0 +1,182 @@ +package analyzer + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/okx/xlayer-toolkit/tools/analysis-check/internal/rpc" +) + +var transferEventSig = common.HexToHash("0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef") + +type Config struct { + InnerTxMode string + BatchSize int +} + +type Result struct { + BlockNumber uint64 + BlockHash common.Hash + TxCount int + AccountCount int + TokenCount int + InnerTxCount int + ElapsedMs int64 +} + +type Analyzer struct { + client *rpc.Client + cfg Config + signer types.Signer +} + +func New(client *rpc.Client, cfg Config) *Analyzer { + chainID, err := client.ChainID(context.Background()) + if err != nil { + panic(err) + } + signer := types.LatestSignerForChainID(chainID) + if signer == nil { + panic(fmt.Errorf("no signer for chain id: %d", chainID)) + } + return &Analyzer{ + client: client, + cfg: cfg, + signer: signer, + } +} + +func (a *Analyzer) AnalyzeBlockByNumber(ctx context.Context, blockNum uint64) (*Result, error) { + start := time.Now() + + block, err := a.client.GetBlockByNumber(ctx, blockNum) + if err != nil { + return nil, fmt.Errorf("get block by number: %w", err) + } + + return a.analyzeBlock(ctx, block, start) +} + +func (a *Analyzer) AnalyzeBlockByHash(ctx context.Context, hash common.Hash) (*Result, error) { + start := time.Now() + + block, err := a.client.GetBlockByHash(ctx, hash) + if err != nil { + return nil, fmt.Errorf("get block by hash: %w", err) + } + + return a.analyzeBlock(ctx, block, start) +} + +func (a *Analyzer) analyzeBlock(ctx context.Context, block *rpc.BlockInfo, start time.Time) (*Result, error) { + receipts, err := a.client.GetBlockReceipts(ctx, block.Number) + if err != nil { + return nil, fmt.Errorf("get block receipts: %w", err) + } + + tokenContracts := make(map[common.Address]struct{}) + for _, receipt := range receipts { + for _, log := range receipt.Logs { + if len(log.Topics) > 0 && log.Topics[0] == transferEventSig { + tokenContracts[log.Address] = struct{}{} + } + } + } + + accounts := make(map[common.Address]struct{}) + for _, tx := range block.Transactions { + if tx.To() != nil { + accounts[*tx.To()] = struct{}{} + } + from, err := types.Sender(a.signer, tx) + if err == nil { + accounts[from] = struct{}{} + } + } + + var innerTxMap map[common.Hash][]rpc.InnerTx + if a.cfg.InnerTxMode == "block" { + innerTxMap, err = a.client.GetBlockInternalTransactions(ctx, block.Number) + if err != nil { + return nil, fmt.Errorf("get block internal transactions: %w", err) + } + } else { + txHashes := make([]common.Hash, len(block.Transactions)) + for i, tx := range block.Transactions { + txHashes[i] = tx.Hash() + } + innerTxMap, err = a.client.BatchGetInternalTransactions(ctx, txHashes, a.cfg.BatchSize) + if err != nil { + return nil, fmt.Errorf("batch get internal transactions: %w", err) + } + } + + innerTxCount := 0 + for _, txInners := range innerTxMap { + for _, inner := range txInners { + innerTxCount++ + if inner.From != "" { + fromAddr := parseAddress(inner.From) + if fromAddr != (common.Address{}) { + accounts[fromAddr] = struct{}{} + } + } + if inner.To != "" { + toAddr := parseAddress(inner.To) + if toAddr != (common.Address{}) { + accounts[toAddr] = struct{}{} + } + } + } + } + + accountList := make([]common.Address, 0, len(accounts)) + for addr := range accounts { + accountList = append(accountList, addr) + } + + tokenList := make([]common.Address, 0, len(tokenContracts)) + for token := range tokenContracts { + tokenList = append(tokenList, token) + } + + _ = a.client.BatchGetBalance(ctx, accountList, block.Number, a.cfg.BatchSize) + _ = a.client.BatchGetTransactionCount(ctx, accountList, block.Number, a.cfg.BatchSize) + + if len(tokenList) > 0 && len(accountList) > 0 { + queries := make([]rpc.ERC20BalanceQuery, 0, len(accountList)*len(tokenList)) + for _, acc := range accountList { + for _, tok := range tokenList { + queries = append(queries, rpc.ERC20BalanceQuery{ + Account: acc, + Token: tok, + }) + } + } + _ = a.client.BatchGetERC20Balances(ctx, queries, block.Number, a.cfg.BatchSize) + } + + elapsed := time.Since(start) + + return &Result{ + BlockNumber: block.Number, + BlockHash: block.Hash, + TxCount: len(block.Transactions), + AccountCount: len(accountList), + TokenCount: len(tokenList), + InnerTxCount: innerTxCount, + ElapsedMs: elapsed.Milliseconds(), + }, nil +} + +func parseAddress(s string) common.Address { + s = strings.TrimSpace(s) + if s == "" { + return common.Address{} + } + return common.HexToAddress(s) +} diff --git a/tools/analysis-check/internal/rpc/client.go b/tools/analysis-check/internal/rpc/client.go new file mode 100644 index 00000000..683ce913 --- /dev/null +++ b/tools/analysis-check/internal/rpc/client.go @@ -0,0 +1,329 @@ +package rpc + +import ( + "context" + "fmt" + "math/big" + "strings" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/ethereum/go-ethereum/rpc" +) + +type Client struct { + rpc *rpc.Client + eth *ethclient.Client +} + +func NewClient(url string) (*Client, error) { + rpcClient, err := rpc.Dial(url) + if err != nil { + return nil, fmt.Errorf("dial rpc: %w", err) + } + ethClient := ethclient.NewClient(rpcClient) + return &Client{ + rpc: rpcClient, + eth: ethClient, + }, nil +} + +func (c *Client) Close() { + c.eth.Close() +} + +func (c *Client) ChainID(ctx context.Context) (*big.Int, error) { + return c.eth.ChainID(ctx) +} + +type BlockInfo struct { + Number uint64 + Hash common.Hash + Transactions types.Transactions +} + +func (c *Client) GetBlockNumber(ctx context.Context) (uint64, error) { + return c.eth.BlockNumber(ctx) +} + +func (c *Client) GetBlockByNumber(ctx context.Context, number uint64) (*BlockInfo, error) { + block, err := c.eth.BlockByNumber(ctx, new(big.Int).SetUint64(number)) + if err != nil { + return nil, err + } + return &BlockInfo{ + Number: block.NumberU64(), + Hash: block.Hash(), + Transactions: block.Transactions(), + }, nil +} + +func (c *Client) GetBlockByHash(ctx context.Context, hash common.Hash) (*BlockInfo, error) { + block, err := c.eth.BlockByHash(ctx, hash) + if err != nil { + return nil, err + } + return &BlockInfo{ + Number: block.NumberU64(), + Hash: block.Hash(), + Transactions: block.Transactions(), + }, nil +} + +type ReceiptLog struct { + Address common.Address + Topics []common.Hash + Data []byte +} + +type Receipt struct { + TxHash common.Hash + Logs []*types.Log +} + +func (c *Client) GetBlockReceipts(ctx context.Context, blockNum uint64) ([]*types.Receipt, error) { + var receipts []*types.Receipt + err := c.rpc.CallContext(ctx, &receipts, "eth_getBlockReceipts", hexutil.EncodeUint64(blockNum)) + if err != nil { + return nil, err + } + return receipts, nil +} + +type InnerTx struct { + From string `json:"from"` + To string `json:"to"` + Value string `json:"value"` +} + +func (c *Client) GetBlockInternalTransactions(ctx context.Context, blockNum uint64) (map[common.Hash][]InnerTx, error) { + var result map[common.Hash][]InnerTx + err := c.rpc.CallContext(ctx, &result, "eth_getBlockInternalTransactions", hexutil.EncodeUint64(blockNum)) + if err != nil { + return nil, err + } + return result, nil +} + +func (c *Client) GetInternalTransactions(ctx context.Context, txHash common.Hash) ([]InnerTx, error) { + var result []InnerTx + err := c.rpc.CallContext(ctx, &result, "eth_getInternalTransactions", txHash) + if err != nil { + return nil, err + } + return result, nil +} + +func (c *Client) BatchGetInternalTransactions(ctx context.Context, txHashes []common.Hash, batchSize int) (map[common.Hash][]InnerTx, error) { + result := make(map[common.Hash][]InnerTx) + + for i := 0; i < len(txHashes); i += batchSize { + end := i + batchSize + if end > len(txHashes) { + end = len(txHashes) + } + batch := txHashes[i:end] + + elems := make([]rpc.BatchElem, len(batch)) + results := make([][]InnerTx, len(batch)) + for j, hash := range batch { + results[j] = []InnerTx{} + elems[j] = rpc.BatchElem{ + Method: "eth_getInternalTransactions", + Args: []interface{}{hash}, + Result: &results[j], + } + } + + if err := c.rpc.BatchCallContext(ctx, elems); err != nil { + return nil, fmt.Errorf("batch call internal transactions: %w", err) + } + + for j, hash := range batch { + if elems[j].Error != nil { + continue + } + result[hash] = results[j] + } + } + + return result, nil +} + +type BatchBalanceResult struct { + Address common.Address + Balance *big.Int + Error error +} + +func (c *Client) BatchGetBalance(ctx context.Context, addresses []common.Address, blockNum uint64, batchSize int) []BatchBalanceResult { + blockTag := hexutil.EncodeUint64(blockNum) + results := make([]BatchBalanceResult, len(addresses)) + + for i := 0; i < len(addresses); i += batchSize { + end := i + batchSize + if end > len(addresses) { + end = len(addresses) + } + batch := addresses[i:end] + + elems := make([]rpc.BatchElem, len(batch)) + balances := make([]*hexutil.Big, len(batch)) + for j, addr := range batch { + elems[j] = rpc.BatchElem{ + Method: "eth_getBalance", + Args: []interface{}{addr, blockTag}, + Result: &balances[j], + } + } + + batchErr := c.rpc.BatchCallContext(ctx, elems) + for j, addr := range batch { + idx := i + j + results[idx].Address = addr + if batchErr != nil { + results[idx].Error = batchErr + } else if elems[j].Error != nil { + results[idx].Error = elems[j].Error + } else if balances[j] != nil { + results[idx].Balance = balances[j].ToInt() + } + } + } + + return results +} + +type BatchNonceResult struct { + Address common.Address + Nonce uint64 + Error error +} + +func (c *Client) BatchGetTransactionCount(ctx context.Context, addresses []common.Address, blockNum uint64, batchSize int) []BatchNonceResult { + blockTag := hexutil.EncodeUint64(blockNum) + results := make([]BatchNonceResult, len(addresses)) + + for i := 0; i < len(addresses); i += batchSize { + end := i + batchSize + if end > len(addresses) { + end = len(addresses) + } + batch := addresses[i:end] + + elems := make([]rpc.BatchElem, len(batch)) + nonces := make([]*hexutil.Uint64, len(batch)) + for j, addr := range batch { + elems[j] = rpc.BatchElem{ + Method: "eth_getTransactionCount", + Args: []interface{}{addr, blockTag}, + Result: &nonces[j], + } + } + + batchErr := c.rpc.BatchCallContext(ctx, elems) + for j, addr := range batch { + idx := i + j + results[idx].Address = addr + if batchErr != nil { + results[idx].Error = batchErr + } else if elems[j].Error != nil { + results[idx].Error = elems[j].Error + } else if nonces[j] != nil { + results[idx].Nonce = uint64(*nonces[j]) + } + } + } + + return results +} + +type ERC20BalanceQuery struct { + Account common.Address + Token common.Address +} + +type BatchERC20BalanceResult struct { + Account common.Address + Token common.Address + Balance *big.Int + Error error +} + +var balanceOfSelector = common.Hex2Bytes("70a08231") + +func (c *Client) BatchGetERC20Balances(ctx context.Context, queries []ERC20BalanceQuery, blockNum uint64, batchSize int) []BatchERC20BalanceResult { + blockTag := hexutil.EncodeUint64(blockNum) + results := make([]BatchERC20BalanceResult, len(queries)) + + for i := 0; i < len(queries); i += batchSize { + end := i + batchSize + if end > len(queries) { + end = len(queries) + } + batch := queries[i:end] + + elems := make([]rpc.BatchElem, len(batch)) + balances := make([]*hexutil.Bytes, len(batch)) + for j, q := range batch { + data := make([]byte, 36) + copy(data[:4], balanceOfSelector) + copy(data[4:], common.LeftPadBytes(q.Account.Bytes(), 32)) + + callArg := map[string]interface{}{ + "to": q.Token, + "data": hexutil.Encode(data), + } + elems[j] = rpc.BatchElem{ + Method: "eth_call", + Args: []interface{}{callArg, blockTag}, + Result: &balances[j], + } + } + + batchErr := c.rpc.BatchCallContext(ctx, elems) + for j, q := range batch { + idx := i + j + results[idx].Account = q.Account + results[idx].Token = q.Token + if batchErr != nil { + results[idx].Error = batchErr + } else if elems[j].Error != nil { + results[idx].Error = elems[j].Error + } else if balances[j] != nil && len(*balances[j]) >= 32 { + results[idx].Balance = new(big.Int).SetBytes(*balances[j]) + } + } + } + + return results +} + +func ParseBlockIdentifier(blockArg string) (num uint64, hash common.Hash, isHash bool, err error) { + blockArg = strings.TrimSpace(blockArg) + if strings.HasPrefix(blockArg, "0x") && len(blockArg) == 66 { + hash = common.HexToHash(blockArg) + isHash = true + return + } + + if strings.HasPrefix(blockArg, "0x") { + n, decErr := hexutil.DecodeUint64(blockArg) + if decErr != nil { + err = fmt.Errorf("invalid block identifier: %s", blockArg) + return + } + num = n + return + } + + n := new(big.Int) + if _, ok := n.SetString(blockArg, 10); !ok { + err = fmt.Errorf("invalid block identifier: %s", blockArg) + return + } + num = n.Uint64() + return +}