Skip to content

feat: add Bitcoin Runes balance display to homepage (#33)#49

Open
hopkdj wants to merge 1 commit intoOWASP-BLT:mainfrom
hopkdj:feature/bitcoin-balance-display
Open

feat: add Bitcoin Runes balance display to homepage (#33)#49
hopkdj wants to merge 1 commit intoOWASP-BLT:mainfrom
hopkdj:feature/bitcoin-balance-display

Conversation

@hopkdj
Copy link
Copy Markdown

@hopkdj hopkdj commented Apr 1, 2026

Summary

Addresses #33 — Show both token chain balances (Bitcoin Runes + Solana SPL) on the homepage.

Changes

Backend (src/index.py):

  • Added new /api/btc-balance endpoint that queries the Hiro Ordinals API for BACON rune balance
  • Uses BITCOIN_WALLET_ADDRESS env var (same pattern as SOLANA_WALLET_ADDRESS)
  • Rune ticker: BLT•BACON•TOKENS
  • 30-second cache (matching existing endpoints)

Frontend (public/index.html):

  • Updated stats grid from 5 to 6 columns
  • Added separate icons for Bitcoin (Runes) and Solana (SPL) chains
  • Added #btc-bacon-balance element showing the Bitcoin BACON balance
  • Added fetch call for /api/btc-balance with comma-formatted display

Before → 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-balance endpoint 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

  • New Features
    • Added Bitcoin Runes support with dedicated stat display for Bitcoin BACON supply
    • Bitcoin BACON balance data now fetches from blockchain and displays with formatting
    • Clarified Solana vs Bitcoin BACON supply distinctions
    • Enhanced stats section layout with improved spacing and organization
    • Updated feature descriptions for clarity

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
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 1, 2026

Walkthrough

This 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 /api/btc-balance endpoint that queries the Hiro Ordinals API for Bitcoin rune holdings.

Changes

Cohort / File(s) Summary
Hero Stats Layout
public/index.html
Updated stats grid from 5 to 6 columns with reduced gaps. Modified Bitcoin/Solana labels, added Bitcoin Runes stat card, relabeled Solana token supply to Solana-specific, changed third stat from "Easy Integration" to "Secure & Transparent". Added client-side fetch logic to retrieve and render Bitcoin balance from /api/btc-balance with thousands separators and fallback to N/A.
Bitcoin Balance Endpoint
src/index.py
Added BACON_RUNE_TICKER and HIRO_RUNES_API constants. Introduced /api/btc-balance route with handle_btc_balance handler that queries Hiro Ordinals API, extracts BACON rune balance for configured wallet address, and responds with balance, ticker, address, and chain identifier. Updated on_fetch routing logic to distinguish between /api/token-supply and /api/btc-balance endpoints with appropriate error handling for missing configuration and upstream API failures.

Sequence Diagram

sequenceDiagram
    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
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested labels

quality: medium

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and specifically describes the main change: adding Bitcoin Runes balance display to the homepage, with a concise and descriptive format.
Linked Issues check ✅ Passed The PR successfully implements requirements from issue #33: displays both token chains (Bitcoin Runes and Solana SPL) with separate UI elements and fetches balance data from the main treasury address via environment-configured wallet.
Out of Scope Changes check ✅ Passed All changes are directly related to issue #33 requirements: backend API for Bitcoin balance, frontend grid expansion for dual-chain display, and icon/label updates to differentiate the two chains.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@owasp-blt
Copy link
Copy Markdown

owasp-blt bot commented Apr 1, 2026

📊 Monthly Leaderboard

Hi @hopkdj! Here's how you rank for April 2026:

Rank User Open PRs PRs (merged) PRs (closed) Reviews Comments Total
42 hhyags @hhyags 2 0 0 0 0 2
43 hopkdj @hopkdj 2 0 0 0 0 2
44 JashwanthMU @JashwanthMU 2 0 0 0 0 2

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 /leaderboard on any issue or PR to see your rank!

Note: Backfill in progress: seeded 1 repos in this run; next page 2. Run /leaderboard again to continue filling historical data.

@owasp-blt
Copy link
Copy Markdown

owasp-blt bot commented Apr 1, 2026

👋 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:

  • The PR author
  • coderabbitai
  • copilot

Once a valid peer review is submitted, this check will pass automatically. Thank you!

⚠️ Peer review enforcement is active.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 returns null balance when BACON rune not found.

When the API call succeeds but the BACON rune isn't in the address's holdings, balance will be null without an error message. The frontend handles this gracefully by displaying "N/A". Consider adding a message field 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

📥 Commits

Reviewing files that changed from the base of the PR and between 99e6b27 and 9e6ba73.

📒 Files selected for processing (2)
  • public/index.html
  • src/index.py

Comment on lines +241 to +248
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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 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}")
EOF

Repository: 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 -80

Repository: 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_TICKER after 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.

@owasp-blt
Copy link
Copy Markdown

owasp-blt bot commented Apr 1, 2026

⚠️ This pull request has 1 unresolved review conversation that must be resolved before merging.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: Ready

Development

Successfully merging this pull request may close these issues.

show the two token chains and the balance on the main address

1 participant