feature: add new price equilibration analysis dashboard#26
feature: add new price equilibration analysis dashboard#26web3skeptic wants to merge 1 commit intomainfrom
Conversation
WalkthroughThe pull request adds a new Price Insights Dashboard accessible via a local page link in index.html. The dashboard displays price snapshots with statistics, price charts with zoom controls, token details tables, and liquidity heatmaps with configurable time ranges and metrics. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant UI as Price Dashboard UI
participant Cache as Token Cache
participant API as Circles API
participant Chart as Chart.js
User->>UI: Load priceInsights.html
activate UI
UI->>API: Fetch snapshots
activate API
API-->>UI: Snapshot list
deactivate API
UI->>UI: Populate snapshot dropdown
UI->>API: Fetch snapshot data
activate API
API-->>UI: Token count, statistics
deactivate API
UI->>API: Fetch liquidity data
activate API
API-->>UI: Liquidity heatmap data
deactivate API
UI->>API: Batch fetch token info
activate API
API-->>UI: Token details (avatar, owner, price)
deactivate API
UI->>Cache: Store token info
activate Cache
Cache-->>UI: Cache updated
deactivate Cache
UI->>Chart: Render price bar chart
Chart-->>UI: Chart rendered
UI->>UI: Populate token table
UI->>UI: Render heatmap
UI-->>User: Dashboard ready
deactivate UI
Note over User,Chart: User selects different snapshot or<br/>adjusts heatmap filters
User->>UI: Change snapshot/filters
activate UI
UI->>Cache: Retrieve cached token info
Cache-->>UI: Cached data
UI->>UI: Regenerate charts and tables
UI-->>User: Updated view
deactivate UI
Estimated code review effort🎯 4 (Complex) | ⏱️ ~65 minutes 🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@priceInsights.html`:
- Around line 875-885: The updateLiquidityStats function currently divides by
data.pairs.length and yields NaN when the array is empty; add an early guard at
the start of updateLiquidityStats that checks if !data.pairs ||
data.pairs.length === 0 and sets sensible defaults (e.g., totalPairs = 0,
avgLiquidity = "0" or "N/A", successRate = "0%" or "N/A", highLiquidityPairs =
0) by updating the DOM elements with IDs totalPairs, avgLiquidity, successRate,
and highLiquidityPairs, then return early; otherwise proceed with the existing
reduce/filter calculations (ensure subsequent calculations use the non-zero
length).
- Around line 889-938: The populateLiquidityTable function currently builds rows
via innerHTML using API-provided fields (e.g., pair.source_avatar,
pair.target_avatar, pair.avg_liquidity) which is an XSS risk; replace this by
either escaping each dynamic value with a helper (e.g., add an escapeHtml(value)
util that sets div.textContent and returns div.innerHTML) or by building table
rows with createElement/textContent and appending children instead of string
templates; update populateLiquidityTable to use escapeHtml or DOM construction
for all interpolated fields (including formatLiquidity(pair.avg_liquidity),
formatSuccessRate(pair.success_rate), src/tgt prices and priceRatio) and apply
the same fix to the token table and error banner rendering functions so no API
value is ever injected directly into innerHTML.
| function updateLiquidityStats(data) { | ||
| document.getElementById('totalPairs').textContent = data.pairs.length; | ||
|
|
||
| const avgLiq = data.pairs.reduce((sum, p) => sum + Number(p.avg_liquidity), 0) / data.pairs.length; | ||
| document.getElementById('avgLiquidity').textContent = formatLiquidity(avgLiq); | ||
|
|
||
| const avgSuccess = data.pairs.reduce((sum, p) => sum + p.success_rate, 0) / data.pairs.length; | ||
| document.getElementById('successRate').textContent = formatSuccessRate(avgSuccess); | ||
|
|
||
| const highLiqPairs = data.pairs.filter(p => Number(p.avg_liquidity) >= 10e18).length; | ||
| document.getElementById('highLiquidityPairs').textContent = highLiqPairs; |
There was a problem hiding this comment.
Guard against empty liquidity datasets to avoid NaN stats.
When no pairs match the filters, averages divide by zero and produce NaN, which leaks into the UI. Add an early return with sane defaults.
✅ Suggested fix
function updateLiquidityStats(data) {
- document.getElementById('totalPairs').textContent = data.pairs.length;
+ if (!data.pairs || data.pairs.length === 0) {
+ document.getElementById('totalPairs').textContent = '0';
+ document.getElementById('avgLiquidity').textContent = '0';
+ document.getElementById('successRate').textContent = '0%';
+ document.getElementById('highLiquidityPairs').textContent = '0';
+ return;
+ }
+
+ document.getElementById('totalPairs').textContent = data.pairs.length;
const avgLiq = data.pairs.reduce((sum, p) => sum + Number(p.avg_liquidity), 0) / data.pairs.length;
document.getElementById('avgLiquidity').textContent = formatLiquidity(avgLiq);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| function updateLiquidityStats(data) { | |
| document.getElementById('totalPairs').textContent = data.pairs.length; | |
| const avgLiq = data.pairs.reduce((sum, p) => sum + Number(p.avg_liquidity), 0) / data.pairs.length; | |
| document.getElementById('avgLiquidity').textContent = formatLiquidity(avgLiq); | |
| const avgSuccess = data.pairs.reduce((sum, p) => sum + p.success_rate, 0) / data.pairs.length; | |
| document.getElementById('successRate').textContent = formatSuccessRate(avgSuccess); | |
| const highLiqPairs = data.pairs.filter(p => Number(p.avg_liquidity) >= 10e18).length; | |
| document.getElementById('highLiquidityPairs').textContent = highLiqPairs; | |
| function updateLiquidityStats(data) { | |
| if (!data.pairs || data.pairs.length === 0) { | |
| document.getElementById('totalPairs').textContent = '0'; | |
| document.getElementById('avgLiquidity').textContent = '0'; | |
| document.getElementById('successRate').textContent = '0%'; | |
| document.getElementById('highLiquidityPairs').textContent = '0'; | |
| return; | |
| } | |
| document.getElementById('totalPairs').textContent = data.pairs.length; | |
| const avgLiq = data.pairs.reduce((sum, p) => sum + Number(p.avg_liquidity), 0) / data.pairs.length; | |
| document.getElementById('avgLiquidity').textContent = formatLiquidity(avgLiq); | |
| const avgSuccess = data.pairs.reduce((sum, p) => sum + p.success_rate, 0) / data.pairs.length; | |
| document.getElementById('successRate').textContent = formatSuccessRate(avgSuccess); | |
| const highLiqPairs = data.pairs.filter(p => Number(p.avg_liquidity) >= 10e18).length; | |
| document.getElementById('highLiquidityPairs').textContent = highLiqPairs; |
🤖 Prompt for AI Agents
In `@priceInsights.html` around lines 875 - 885, The updateLiquidityStats function
currently divides by data.pairs.length and yields NaN when the array is empty;
add an early guard at the start of updateLiquidityStats that checks if
!data.pairs || data.pairs.length === 0 and sets sensible defaults (e.g.,
totalPairs = 0, avgLiquidity = "0" or "N/A", successRate = "0%" or "N/A",
highLiquidityPairs = 0) by updating the DOM elements with IDs totalPairs,
avgLiquidity, successRate, and highLiquidityPairs, then return early; otherwise
proceed with the existing reduce/filter calculations (ensure subsequent
calculations use the non-zero length).
| function populateLiquidityTable(data) { | ||
| const tbody = document.getElementById('liquidityPairTableBody'); | ||
|
|
||
| // Sort by average liquidity | ||
| const sorted = [...data.pairs].sort((a, b) => | ||
| Number(b.avg_liquidity) - Number(a.avg_liquidity) | ||
| ); | ||
|
|
||
| tbody.innerHTML = sorted.map((pair, index) => { | ||
| const avgCrc = Number(pair.avg_liquidity) / 1e18; | ||
| const liquidityClass = avgCrc >= 10 ? 'liquidity-high' : | ||
| avgCrc >= 1 ? 'liquidity-medium' : 'liquidity-low'; | ||
|
|
||
| const priceRatio = Number(pair.avg_price_ratio || 0); | ||
| const priceRatioClass = priceRatio < 0.5 ? 'liquidity-high' : | ||
| priceRatio < 0.9 ? 'liquidity-medium' : 'liquidity-low'; | ||
|
|
||
| // Format prices | ||
| const srcPrice = pair.avg_source_price ? (Number(pair.avg_source_price) / 1e18).toFixed(4) : 'N/A'; | ||
| const tgtPrice = pair.avg_target_price ? (Number(pair.avg_target_price) / 1e18).toFixed(4) : 'N/A'; | ||
|
|
||
| // Determine opportunity based on price ratio and liquidity | ||
| let opportunity; | ||
| if (priceRatio < 0.5 && avgCrc >= 1) { | ||
| opportunity = 'Excellent'; | ||
| } else if (priceRatio < 0.7 && avgCrc >= 0.1) { | ||
| opportunity = 'Good'; | ||
| } else if (priceRatio < 0.9) { | ||
| opportunity = 'Moderate'; | ||
| } else if (avgCrc >= 10) { | ||
| opportunity = 'High Liquidity'; | ||
| } else { | ||
| opportunity = 'Limited'; | ||
| } | ||
|
|
||
| return ` | ||
| <tr> | ||
| <td>${index + 1}</td> | ||
| <td style="font-size: 0.65em; word-break: break-all;">${pair.source_avatar}</td> | ||
| <td style="font-size: 0.65em; word-break: break-all;">${pair.target_avatar}</td> | ||
| <td class="${liquidityClass}">${formatLiquidity(pair.avg_liquidity)}</td> | ||
| <td>${srcPrice}</td> | ||
| <td>${tgtPrice}</td> | ||
| <td class="${priceRatioClass}">${priceRatio.toFixed(3)}</td> | ||
| <td>${formatSuccessRate(pair.success_rate)}</td> | ||
| <td>${pair.observation_count}</td> | ||
| <td>${opportunity}</td> | ||
| </tr> | ||
| `; | ||
| }).join(''); |
There was a problem hiding this comment.
Avoid injecting API data via innerHTML (XSS risk).
API-sourced fields (avatars, addresses, pool values) are interpolated into HTML strings. If any field is compromised or malformed, this enables script injection. Prefer DOM construction with textContent, or escape values before interpolation. Please apply the same approach to the token table and error banner rendering too.
🔒 Suggested fix (escape dynamic fields)
- return `
+ return `
<tr>
<td>${index + 1}</td>
- <td style="font-size: 0.65em; word-break: break-all;">${pair.source_avatar}</td>
- <td style="font-size: 0.65em; word-break: break-all;">${pair.target_avatar}</td>
+ <td style="font-size: 0.65em; word-break: break-all;">${escapeHtml(pair.source_avatar)}</td>
+ <td style="font-size: 0.65em; word-break: break-all;">${escapeHtml(pair.target_avatar)}</td>
<td class="${liquidityClass}">${formatLiquidity(pair.avg_liquidity)}</td>
<td>${srcPrice}</td>
<td>${tgtPrice}</td>
<td class="${priceRatioClass}">${priceRatio.toFixed(3)}</td>
<td>${formatSuccessRate(pair.success_rate)}</td>
<td>${pair.observation_count}</td>
<td>${opportunity}</td>
</tr>
`;Add a small helper near the other utility functions:
function escapeHtml(value) {
const div = document.createElement('div');
div.textContent = value ?? '';
return div.innerHTML;
}🤖 Prompt for AI Agents
In `@priceInsights.html` around lines 889 - 938, The populateLiquidityTable
function currently builds rows via innerHTML using API-provided fields (e.g.,
pair.source_avatar, pair.target_avatar, pair.avg_liquidity) which is an XSS
risk; replace this by either escaping each dynamic value with a helper (e.g.,
add an escapeHtml(value) util that sets div.textContent and returns
div.innerHTML) or by building table rows with createElement/textContent and
appending children instead of string templates; update populateLiquidityTable to
use escapeHtml or DOM construction for all interpolated fields (including
formatLiquidity(pair.avg_liquidity), formatSuccessRate(pair.success_rate),
src/tgt prices and priceRatio) and apply the same fix to the token table and
error banner rendering functions so no API value is ever injected directly into
innerHTML.
Preview available at https://web3skeptic.github.io/CirclesTools/priceInsights.html (might load for few seconds to get observations)
Summary by CodeRabbit
Release Notes
New Features
Updates
✏️ Tip: You can customize this high-level summary in your review settings.