-
Notifications
You must be signed in to change notification settings - Fork 73
feat: add new report cmd #812
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
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR adds a new report command to generate comprehensive blockchain analysis reports for Ethereum-compatible chains. The command fetches block and transaction data via RPC, calculates statistics, and outputs results in JSON, HTML, or PDF format with support for concurrent requests and rate limiting.
Key changes:
- New report command with JSON/HTML/PDF output formats
- Concurrent block fetching (configurable, default 10 workers) with rate limiting (default 4 req/s)
- Comprehensive metrics including transaction counts, gas usage, unique addresses, and top-10 analyses
- HTML visualizations with SVG charts and interactive tooltips
- PDF generation using chromedp for headless browser rendering
Reviewed changes
Copilot reviewed 8 out of 9 changed files in this pull request and generated 15 comments.
Show a summary per file
| File | Description |
|---|---|
| go.mod / go.sum | Added chromedp dependencies for PDF generation and viper v1.19.0 (moved from direct to indirect dependency) with related transitive dependency version adjustments |
| cmd/root.go | Registered the new report command in the CLI |
| cmd/report/report.go | Core implementation: flag validation, concurrent RPC fetching with rate limiting, block/transaction data processing, and output generation |
| cmd/report/types.go | Data structures for report, summary statistics, block info, transaction info, and top-10 analyses |
| cmd/report/html.go | HTML generation functions including stat cards, SVG line charts for transactions and gas usage, and top-10 tables |
| cmd/report/pdf.go | PDF generation using chromedp to render HTML to PDF with page settings |
| cmd/report/template.html | HTML template with embedded CSS for styling and JavaScript for chart tooltips |
| cmd/report/usage.md | Documentation covering features, usage examples, and report contents |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| <td><code>%s</code></td> | ||
| <td>%s</td> | ||
| <td>%s</td> | ||
| <td>%s</td> | ||
| </tr>`, i+1, tx.Hash, formatNumber(tx.BlockNumber), formatNumberWithUnits(tx.GasLimit), formatNumber(tx.GasUsed))) | ||
| } | ||
|
|
||
| sb.WriteString(` | ||
| </tbody> | ||
| </table> | ||
| </div>`) | ||
| } | ||
|
|
||
| // Top 10 transactions by gas limit | ||
| if len(report.Top10.TransactionsByGasLimit) > 0 { | ||
| sb.WriteString(` | ||
| <div class="subsection"> | ||
| <h3>Top 10 Transactions by Gas Limit</h3> | ||
| <table> | ||
| <thead> | ||
| <tr> | ||
| <th>Rank</th> | ||
| <th>Transaction Hash</th> | ||
| <th>Block Number</th> | ||
| <th>Gas Limit</th> | ||
| <th>Gas Used (Wei)</th> | ||
| </tr> | ||
| </thead> | ||
| <tbody>`) | ||
|
|
||
| for i, tx := range report.Top10.TransactionsByGasLimit { | ||
| sb.WriteString(fmt.Sprintf(` | ||
| <tr> | ||
| <td>%d</td> | ||
| <td><code>%s</code></td> |
Copilot
AI
Dec 29, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Transaction hashes from the blockchain are directly embedded into HTML without escaping at lines 500 and 534. While transaction hashes are typically hexadecimal strings, if the RPC endpoint returns unexpected data, this could lead to HTML injection. Use html.EscapeString() to safely escape all dynamic content before embedding in HTML.
| ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) | ||
| defer cancel() | ||
|
|
||
| // Allocate a new browser context | ||
| ctx, cancel = chromedp.NewContext(ctx) | ||
| defer cancel() |
Copilot
AI
Dec 29, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The cancel function returned from chromedp.NewContext is assigned to the same variable as the previous cancel function from context.WithTimeout, overwriting it. This means the first cancel is never called, potentially leaking the timeout context. Store the second cancel in a different variable (e.g., cancel2) and defer it separately, or ensure both cancels are invoked.
| ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) | |
| defer cancel() | |
| // Allocate a new browser context | |
| ctx, cancel = chromedp.NewContext(ctx) | |
| defer cancel() | |
| ctx, cancelTimeout := context.WithTimeout(context.Background(), 30*time.Second) | |
| defer cancelTimeout() | |
| // Allocate a new browser context | |
| ctx, cancelBrowser := chromedp.NewContext(ctx) | |
| defer cancelBrowser() |
| padding, height-padding+15, report.Blocks[0].Number, | ||
| width-padding, height-padding+15, report.Blocks[len(report.Blocks)-1].Number, |
Copilot
AI
Dec 29, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The axis labels at lines 152-153 display report.Blocks[0].Number and report.Blocks[len(report.Blocks)-1].Number, assuming these are the first and last blocks in the requested range. However, because blocks are added in the order they arrive from concurrent workers (not sorted by block number), these labels may not correspond to the actual start and end of the block range specified by the user. This will create misleading charts where the axis labels don't match the displayed data. This depends on fixing the block ordering issue at line 207 of report.go.
| padding, height-padding+15, report.Blocks[0].Number, | ||
| width-padding, height-padding+15, report.Blocks[len(report.Blocks)-1].Number, |
Copilot
AI
Dec 29, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The axis labels at lines 242-243 have the same issue as the transaction count chart: they display report.Blocks[0].Number and report.Blocks[len(report.Blocks)-1].Number assuming these correspond to the start and end of the user-specified range. Because blocks are added in arrival order (not sorted by block number), these labels will be incorrect and misleading. This depends on fixing the block ordering issue at line 207 of report.go.
cmd/report/html.go
Outdated
| numPoints := 0 | ||
| for i := 0; i < len(report.Blocks); i += step { | ||
| block := report.Blocks[i] | ||
| x := padding + (float64(numPoints) / float64((len(report.Blocks)-1)/step)) * chartWidth |
Copilot
AI
Dec 29, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When there is only one block (len(report.Blocks) == 1), the expression float64((len(report.Blocks)-1)/step) at line 121 evaluates to 0, causing division by zero which results in +Inf for the x coordinate. This will produce invalid SVG coordinates. Add a check for single-block edge case or ensure the denominator is at least 1.
|
|
||
| ## Notes | ||
|
|
||
| - The command queries blocks sequentially to avoid overwhelming the RPC endpoint |
Copilot
AI
Dec 29, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The documentation states "The command queries blocks sequentially to avoid overwhelming the RPC endpoint" but the actual implementation uses concurrent workers (default: 10) as specified in the PR description and implemented in report.go. This is misleading. Update the documentation to accurately reflect that blocks are fetched concurrently with configurable concurrency and rate limiting.
| - The command queries blocks sequentially to avoid overwhelming the RPC endpoint | |
| - The command queries blocks concurrently using a worker pool (default: 10 workers), with configurable concurrency and rate limiting to avoid overwhelming the RPC endpoint |
| blockInfo, err := fetchBlockInfo(ctx, ec, blockNum, rateLimiter) | ||
| if err != nil { | ||
| errorChan <- fmt.Errorf("failed to fetch block %d: %w", blockNum, err) | ||
| return | ||
| } |
Copilot
AI
Dec 29, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a worker encounters an error at line 174, it sends one error to errorChan and returns immediately. However, other workers continue processing blocks and may send results to resultChan. The main loop will return on the first error (line 235), but remaining goroutines are not canceled, potentially causing resource leaks or hanging goroutines. Consider using a context cancellation mechanism to stop all workers when the first error occurs, or draining the channels properly before returning.
|
|
||
| // Replace metadata placeholders | ||
| html = strings.ReplaceAll(html, "{{CHAIN_ID}}", fmt.Sprintf("%d", report.ChainID)) | ||
| html = strings.ReplaceAll(html, "{{RPC_URL}}", report.RpcUrl) |
Copilot
AI
Dec 29, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The RPC URL from user input is directly embedded into HTML without escaping at line 19. If the RPC URL contains HTML special characters (e.g., <, >, &, quotes), it could break the HTML structure or potentially lead to XSS if the URL contains malicious content. Use html.EscapeString() to properly escape the RPC URL and other user-controlled data before inserting into HTML.
| // Wait a bit for any dynamic content to settle | ||
| time.Sleep(500 * time.Millisecond) | ||
| return nil |
Copilot
AI
Dec 29, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using time.Sleep for a fixed 500ms delay is a blocking operation that doesn't respect context cancellation. If the context is canceled during this sleep, the function will still wait the full duration. Use a timer with context cancellation instead, such as select { case <-time.After(500*time.Millisecond): case <-ctx.Done(): return ctx.Err() }.
| // Wait a bit for any dynamic content to settle | |
| time.Sleep(500 * time.Millisecond) | |
| return nil | |
| // Wait a bit for any dynamic content to settle, respecting context cancellation | |
| select { | |
| case <-time.After(500 * time.Millisecond): | |
| return nil | |
| case <-ctx.Done(): | |
| return ctx.Err() | |
| } |
cmd/report/html.go
Outdated
| numPoints := 0 | ||
| for i := 0; i < len(report.Blocks); i += step { | ||
| block := report.Blocks[i] | ||
| x := padding + (float64(numPoints) / float64((len(report.Blocks)-1)/step)) * chartWidth |
Copilot
AI
Dec 29, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When there is only one block (len(report.Blocks) == 1), the expression float64((len(report.Blocks)-1)/step) at line 211 evaluates to 0, causing division by zero which results in +Inf for the x coordinate. This will produce invalid SVG coordinates. Add a check for single-block edge case or ensure the denominator is at least 1.
closes https://github.com/0xPolygon/devtools/issues/479
Adds a new report command to generate comprehensive blockchain analysis reports for Ethereum-compatible chains. Supports JSON, HTML, and PDF output formats with statistics, visualizations, and performance optimizations.
Features
Usage
JSON
HTML
PDF