feat: add Bitcoin Runes balance display to homepage (#33)#49
feat: add Bitcoin Runes balance display to homepage (#33)#49hopkdj wants to merge 1 commit intoOWASP-BLT:mainfrom
Conversation
Show both token chain balances (Bitcoin Runes + Solana SPL) on the main page, addressing issue OWASP-BLT#33. Changes: - Added /api/btc-balance endpoint in src/index.py that queries the Hiro Ordinals API for BACON rune balance on the treasury address - Updated public/index.html stats section to display 6 columns including the new Bitcoin Runes BACON balance - BITCOIN_WALLET_ADDRESS env var needed for the new endpoint - Rune ticker: BLT•BACON•TOKENS
WalkthroughThis change adds Bitcoin (Runes) balance display to the main page alongside existing Solana (SPL) balance, updating the hero stats section layout from 5 to 6 columns and implementing a new Changes
Sequence DiagramsequenceDiagram
participant Browser as Browser
participant Server as Backend Server
participant HiroAPI as Hiro Ordinals API
Browser->>Server: GET /api/btc-balance
Server->>Server: Read BITCOIN_WALLET_ADDRESS from env
Server->>HiroAPI: GET /runes/v1/addresses/{address}/balances
alt Successful Response
HiroAPI-->>Server: Return rune balances list
Server->>Server: Extract BACON rune entry
Server-->>Browser: JSON {balance, ticker, address, chain}
else Upstream Error
HiroAPI-->>Server: HTTP error
Server-->>Browser: Error response
else Missing Config
Server-->>Browser: Config error response
end
Browser->>Browser: Format balance with separators
Browser->>Browser: Render into btc-bacon-balance element
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Suggested labels
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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 |
📊 Monthly LeaderboardHi @hopkdj! Here's how you rank for April 2026:
Scoring this month (across OWASP-BLT org): Open PRs (+1 each), Merged PRs (+10), Closed (not merged) (−2), Reviews (+5; first two per PR in-month), Comments (+2, excludes CodeRabbit). Run
|
|
👋 Hi @hopkdj! This pull request needs a peer review before it can be merged. Please request a review from a team member who is not:
Once a valid peer review is submitted, this check will pass automatically. Thank you!
|
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (3)
public/index.html (1)
174-177: Duplicated number formatting logic.The comma-formatting code (splitting by
., applying regex for thousands separator) is duplicated from the BACON supply handler above. Consider extracting a shared helper function for maintainability.♻️ Optional: Extract formatting helper
<script> (function () { + function formatWithCommas(value) { + var parts = String(value).split('.'); + parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ','); + return parts.join('.'); + } + var solEl = document.getElementById('sol-balance'); // ... sol-balance fetch unchanged ... var supplyEl = document.getElementById('bacon-supply'); fetch('/api/token-supply') .then(function (r) { return r.json(); }) .then(function (data) { if (data.supply !== null && data.supply !== undefined) { - var parts = String(data.supply).split('.'); - parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ','); - supplyEl.textContent = parts.join('.'); + supplyEl.textContent = formatWithCommas(data.supply); } else { supplyEl.textContent = 'N/A'; } }) // ... var btcEl = document.getElementById('btc-bacon-balance'); fetch('/api/btc-balance') .then(function (r) { return r.json(); }) .then(function (data) { if (data.balance !== null && data.balance !== undefined) { - var parts = String(data.balance).split('.'); - parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ','); - btcEl.textContent = parts.join('.'); + btcEl.textContent = formatWithCommas(data.balance); } else { btcEl.textContent = 'N/A'; } }) // ... })(); </script>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@public/index.html` around lines 174 - 177, Extract the duplicated comma-formatting logic into a single helper function (e.g., formatWithCommas or formatCurrency) that accepts a numeric or string value, splits on '.', applies the thousands-separator regex to the integer part, and rejoins the parts; then replace the inline logic that manipulates parts and sets btcEl.textContent (using data.balance) with a call to that helper, and do the same for the BACON supply handler so both locations reuse the shared function.src/index.py (2)
250-258: Response returnsnullbalance when BACON rune not found.When the API call succeeds but the BACON rune isn't in the address's holdings,
balancewill benullwithout an error message. The frontend handles this gracefully by displaying "N/A". Consider adding amessagefield to clarify when the rune wasn't found vs when there's a zero balance.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/index.py` around lines 250 - 258, The response currently sets 'balance' (bacon_balance) to null when the BACON rune isn't found; update the return in Response.new to include an explicit 'message' field that distinguishes "rune not found" vs "zero balance" (use the existing BACON_RUNE_TICKER and btc_address context), e.g., set message to something like "BACON rune not found for address" when bacon_balance is None and to "OK" or "Zero balance" when present; implement this logic where bacon_balance is computed and returned in the Response.new block so consumers of Response.new can display a clear message.
219-227: Consider adding a timeout for the upstream API call.The fetch to Hiro API has no explicit timeout. While Cloudflare Workers have execution time limits, a slow upstream response could consume resources unnecessarily. This matches existing handlers, so it's a codebase-wide consideration rather than specific to this change.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/index.py` around lines 219 - 227, The upstream fetch to api_url (assigning api_response) lacks a timeout; wrap the fetch call with an AbortController (or the project's existing timeout helper used by other handlers) and start a timer before calling fetch that aborts the controller after a configured timeout (e.g., 5–10s), pass controller.signal into the fetch options, and clear the timer after fetch completes to avoid leaks; update the fetch invocation that sets api_response and ensure error handling treats AbortError/timeouts similarly to other handlers.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/index.py`:
- Around line 241-248: The current check in the loop over data uses a no-op
replace and a fragile substring match; update the matching logic in the loop
that inspects rune_name (and uses BACON_RUNE_TICKER and sets bacon_balance) to
normalize both strings (e.g., strip whitespace and casefold or use Unicode
normalization) and then perform either an exact equality against
BACON_RUNE_TICKER or a token-aware match: split rune_name on common separators
(e.g., '•', '|', '/', spaces, commas) and compare tokens to BACON_RUNE_TICKER
(also normalized) or check for a whole-token 'bacon' match instead of a raw
substring; keep the existing amount extraction (getattr(entry, 'amount')) and
bacon_balance assignment unchanged once a token/equality match succeeds.
---
Nitpick comments:
In `@public/index.html`:
- Around line 174-177: Extract the duplicated comma-formatting logic into a
single helper function (e.g., formatWithCommas or formatCurrency) that accepts a
numeric or string value, splits on '.', applies the thousands-separator regex to
the integer part, and rejoins the parts; then replace the inline logic that
manipulates parts and sets btcEl.textContent (using data.balance) with a call to
that helper, and do the same for the BACON supply handler so both locations
reuse the shared function.
In `@src/index.py`:
- Around line 250-258: The response currently sets 'balance' (bacon_balance) to
null when the BACON rune isn't found; update the return in Response.new to
include an explicit 'message' field that distinguishes "rune not found" vs "zero
balance" (use the existing BACON_RUNE_TICKER and btc_address context), e.g., set
message to something like "BACON rune not found for address" when bacon_balance
is None and to "OK" or "Zero balance" when present; implement this logic where
bacon_balance is computed and returned in the Response.new block so consumers of
Response.new can display a clear message.
- Around line 219-227: The upstream fetch to api_url (assigning api_response)
lacks a timeout; wrap the fetch call with an AbortController (or the project's
existing timeout helper used by other handlers) and start a timer before calling
fetch that aborts the controller after a configured timeout (e.g., 5–10s), pass
controller.signal into the fetch options, and clear the timer after fetch
completes to avoid leaks; update the fetch invocation that sets api_response and
ensure error handling treats AbortError/timeouts similarly to other handlers.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 088b880a-949d-4a26-8f89-9c1f78261dab
📒 Files selected for processing (2)
public/index.htmlsrc/index.py
| for entry in data: | ||
| rune_name = getattr(entry, 'rune', '') or '' | ||
| if BACON_RUNE_TICKER.replace('•', '•').lower() in rune_name.lower() or 'bacon' in rune_name.lower(): | ||
| # Entry has 'amount' as a string | ||
| amount = getattr(entry, 'amount', None) | ||
| if amount is not None: | ||
| bacon_balance = str(amount) | ||
| break |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Check Hiro API documentation for rune balance response structure
# Search for any existing tests or documentation about Hiro API response format
rg -n -C3 'hiro' --type=py
# Check if there are any API docs or examples in the repo
fd -i 'readme' --exec cat {}Repository: OWASP-BLT/BLT-Rewards
Length of output: 8039
🏁 Script executed:
cat -n src/index.py | sed -n '199,263p'Repository: OWASP-BLT/BLT-Rewards
Length of output: 2838
🏁 Script executed:
# Check the exact byte representation of the bullet characters
python3 << 'EOF'
import re
# Read the file
with open('src/index.py', 'r', encoding='utf-8') as f:
content = f.read()
# Find the BACON_RUNE_TICKER definition
match = re.search(r'BACON_RUNE_TICKER\s*=\s*"([^"]+)"', content)
if match:
ticker = match.group(1)
print(f"BACON_RUNE_TICKER value: {ticker}")
print(f"Characters and their unicode points:")
for i, char in enumerate(ticker):
print(f" [{i}] '{char}' = U+{ord(char):04X}")
# Find the replace call at line 243
lines = content.split('\n')
for i, line in enumerate(lines[240:250], start=241):
print(f"\n{i}: {line}")
if 'replace' in line:
# Extract the characters in the replace call
replace_match = re.search(r"replace\('([^']+)',\s*'([^']+)'\)", line)
if replace_match:
char1 = replace_match.group(1)
char2 = replace_match.group(2)
print(f" First arg: '{char1}' = U+{ord(char1):04X}")
print(f" Second arg: '{char2}' = U+{ord(char2):04X}")
print(f" Same? {char1 == char2}")
EOFRepository: OWASP-BLT/BLT-Rewards
Length of output: 1020
🏁 Script executed:
# Check for Hiro API response structure examples or tests
rg -n 'data\|rune\|amount' src/index.py -A2 -B2 | head -80Repository: OWASP-BLT/BLT-Rewards
Length of output: 47
Line 243: The replace('•', '•') is a no-op and substring matching is fragile.
The replace('•', '•') replaces the bullet character with an identical character (both U+2022), doing nothing. Additionally, the fallback 'bacon' in rune_name.lower() could unintentionally match runes with "bacon" as a substring (e.g., SMOKY•BACON•BITS).
Consider a more precise match:
- Exact equality check against
BACON_RUNE_TICKERafter normalizing both strings - Limit substring matching to avoid accidental token matches, or switch to token-aware parsing
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/index.py` around lines 241 - 248, The current check in the loop over data
uses a no-op replace and a fragile substring match; update the matching logic in
the loop that inspects rune_name (and uses BACON_RUNE_TICKER and sets
bacon_balance) to normalize both strings (e.g., strip whitespace and casefold or
use Unicode normalization) and then perform either an exact equality against
BACON_RUNE_TICKER or a token-aware match: split rune_name on common separators
(e.g., '•', '|', '/', spaces, commas) and compare tokens to BACON_RUNE_TICKER
(also normalized) or check for a whole-token 'bacon' match instead of a raw
substring; keep the existing amount extraction (getattr(entry, 'amount')) and
bacon_balance assignment unchanged once a token/equality match succeeds.
|
|
Summary
Addresses #33 — Show both token chain balances (Bitcoin Runes + Solana SPL) on the homepage.
Changes
Backend (src/index.py):
/api/btc-balanceendpoint that queries the Hiro Ordinals API for BACON rune balanceBITCOIN_WALLET_ADDRESSenv var (same pattern asSOLANA_WALLET_ADDRESS)BLT•BACON•TOKENSFrontend (public/index.html):
#btc-bacon-balanceelement showing the Bitcoin BACON balance/api/btc-balancewith comma-formatted displayBefore → After
Before: 5 stats — Multi-Chain icon, Secure icon, Rocket icon, SOL balance, BACON supply
After: 6 stats — Bitcoin icon, Solana icon, Secure icon, SOL balance, BACON supply (Solana), BACON balance (Bitcoin Runes)
Testing
The
/api/btc-balanceendpoint will return{balance: null, error: "BITCOIN_WALLET_ADDRESS not configured"}until the env var is set on Cloudflare Workers.Closes #33
Summary by CodeRabbit
Release Notes