Skip to content

SvelteKit block explorer with real RPC integration#110

Merged
Steake merged 5 commits intomasterfrom
copilot/build-block-explorer-visualization
Dec 11, 2025
Merged

SvelteKit block explorer with real RPC integration#110
Steake merged 5 commits intomasterfrom
copilot/build-block-explorer-visualization

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Dec 9, 2025

Implements comprehensive block explorer for RC3-004 using SvelteKit with real-time RPC connections to BitCell nodes.

Architecture

Complete Refactor: Removed mock data implementation and created standalone SvelteKit application that connects to live blockchain nodes via JSON-RPC.

New SvelteKit Application (crates/bitcell-explorer/)

Technology Stack:

  • Framework: SvelteKit with static adapter (client-side only SPA)
  • Build: Vite with RPC proxy configuration
  • Styling: Custom CSS with cyberpunk theme
  • No SSR (as requested)

RPC Client (src/lib/rpc.js):
Connects to BitCell node JSON-RPC endpoints:

  • eth_blockNumber - Current block height
  • eth_getBlockByNumber - Block details with transactions
  • eth_getTransactionByHash - Transaction details
  • eth_getBalance - Account balance
  • eth_getTransactionCount - Account nonce
  • bitcell_getNodeInfo - Node information
  • bitcell_getTournamentState - Tournament state
  • bitcell_getBattleReplay - Battle replay data
  • bitcell_getMinerStats - Miner EBSL statistics

Features:

  • Universal search (block height, transaction hash, account address)
  • Real-time block list with auto-refresh
  • Transaction details with status
  • Account information with balance and nonce
  • EBSL trust score integration
  • Battle visualization support

Removed

  • crates/bitcell-admin/src/api/explorer.rs - Mock API endpoints (280 lines)
  • crates/bitcell-admin/src/web/explorer.rs - Inline HTML/JS explorer (771 lines)
  • ❌ All dummy/mocked data
  • ❌ Explorer routes from admin console

Security & Accessibility

Security Improvements:

  • Proper hex validation with character checks for all addresses and hashes
  • XSS protection through Svelte's built-in escaping (no inline onclick handlers)
  • Input sanitization with regex patterns
  • Validation helper functions in src/lib/utils.js

Accessibility Improvements:

  • ARIA labels on all interactive elements
  • Keyboard navigation support
  • Semantic HTML structure
  • Focus indicators for accessibility

Development

cd crates/bitcell-explorer

# Install dependencies
npm install

# Development server (localhost:5173)
npm run dev

# Production build
npm run build

Configuration

The explorer proxies /rpc requests to the BitCell node. Default configuration in vite.config.js:

server: {
  proxy: {
    '/rpc': {
      target: 'http://localhost:9545',  // BitCell node RPC port
      changeOrigin: true
    }
  }
}

Code Review Resolutions

Addressed all 15 code review comments:

  • ✅ Removed magic numbers (using real blockchain timestamps)
  • ✅ Fixed XSS vulnerabilities (Svelte escaping, no inline handlers)
  • ✅ Added proper hex validation for addresses, hashes
  • ✅ Improved accessibility (ARIA labels, keyboard navigation)
  • ✅ Eliminated code duplication (utility functions)
  • ✅ Fixed event handling (proper Svelte event bindings)
  • ✅ Updated documentation for accuracy

Testing

  • ✅ Cargo build succeeds for bitcell-admin
  • ✅ All tests pass (10/10)
  • ✅ No compilation errors
  • ✅ TypeScript/JSDoc type checking available

Integration

The explorer is a standalone application that can be:

  1. Served from the admin console by serving built static files
  2. Deployed independently with nginx/Apache proxying /rpc to BitCell node
  3. Developed independently with hot reload

Updated documentation in docs/BLOCK_EXPLORER.md with deployment instructions and architecture details.

Screenshots

Note: Screenshots show the previous inline implementation. The new SvelteKit application maintains the same visual design and cyberpunk aesthetic while connecting to real blockchain data.

Original prompt

This section details on the original issue you should resolve

<issue_title>Build Block Explorer with Tournament Visualization</issue_title>
<issue_description>Part of Epic: #78 - RC3: Developer Ecosystem & Tools


Goal: Create comprehensive block explorer with tournament replay visualization.

Requirements:

  • Block and transaction detail pages
  • Account balance/transaction history
  • Tournament battle replay with grid visualization
  • Search by hash, address, block height
  • Trust score display

Acceptance Criteria:

  • All blockchain data viewable
  • Battle visualization shows CA evolution
  • Search and filtering functional

See [RELEASE_REQUIREMENTS.md](https://github.com/Steake/BitCell/blob/master/docs/RELEASE_REQUIREMENTS. md)</issue_description>

Comments on the Issue (you are @copilot in this section)


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Copilot AI and others added 3 commits December 9, 2025 12:42
Co-authored-by: Steake <530040+Steake@users.noreply.github.com>
Co-authored-by: Steake <530040+Steake@users.noreply.github.com>
Co-authored-by: Steake <530040+Steake@users.noreply.github.com>
Copilot AI changed the title [WIP] Create block explorer with tournament visualization Implement block explorer with tournament visualization and trust score tracking Dec 9, 2025
Copilot AI requested a review from Steake December 9, 2025 13:03
@Steake Steake marked this pull request as ready for review December 9, 2025 13:04
Copilot AI review requested due to automatic review settings December 9, 2025 13:04
Copy link
Copy Markdown

Copilot AI left a 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 implements a comprehensive block explorer interface for the BitCell blockchain, providing web UI and API endpoints for viewing blocks, transactions, accounts, and trust scores. The implementation uses mock data currently and documents the integration points needed for connecting real blockchain data sources (StateManager, Blockchain, EBSL).

Key changes:

  • New block explorer API with search, transaction details, account information, and trust score endpoints
  • Cyberpunk-themed web interface with modals for detailed views
  • Integration with existing battle visualization from the dashboard
  • Comprehensive documentation of features and future enhancements

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 15 comments.

Show a summary per file
File Description
docs/BLOCK_EXPLORER.md Complete documentation of explorer features, API endpoints, usage examples, and integration notes
crates/bitcell-admin/src/api/explorer.rs API endpoints for transactions, accounts, trust scores, and universal search with mock data
crates/bitcell-admin/src/web/explorer.rs Single-page web interface with search, block/transaction/account modals, and trust score visualization
crates/bitcell-admin/src/api/mod.rs Adds explorer module to API exports
crates/bitcell-admin/src/web/mod.rs Adds explorer module to web exports
crates/bitcell-admin/src/lib.rs Registers explorer web route and API endpoints in the router

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +102 to +164
timestamp: 1700000000,
status: "confirmed".to_string(),
}))
}

/// Get account information and balance
pub async fn get_account(
State(_state): State<Arc<AppState>>,
Path(address): Path<String>,
) -> Result<Json<AccountInfo>, (StatusCode, Json<String>)> {
// In a real implementation, this would query the state manager
// For now, return mock data

// Validate address format
if !address.starts_with("0x") || address.len() != 42 {
return Err((
StatusCode::BAD_REQUEST,
Json("Invalid address format".to_string()),
));
}

// Mock trust score calculation
let trust_score = 0.85;

Ok(Json(AccountInfo {
address: address.clone(),
balance: 5000000, // 5 CELL
nonce: 10,
transaction_count: 25,
trust_score,
is_miner: trust_score > 0.75,
}))
}

/// Get transaction history for an account
pub async fn get_account_transactions(
State(_state): State<Arc<AppState>>,
Path(address): Path<String>,
Query(pagination): Query<PaginationQuery>,
) -> Result<Json<TransactionHistoryResponse>, (StatusCode, Json<String>)> {
// Validate address format
if !address.starts_with("0x") || address.len() != 42 {
return Err((
StatusCode::BAD_REQUEST,
Json("Invalid address format".to_string()),
));
}

// In a real implementation, this would query the transaction index
// For now, return mock data
let mut transactions = Vec::new();

// Generate some mock transactions
for i in 0..10 {
transactions.push(TransactionDetail {
hash: format!("0x{:064x}", i * 12345),
block_height: 12340 + i,
from: if i % 2 == 0 { address.clone() } else { "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb8".to_string() },
to: if i % 2 == 0 { "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb8".to_string() } else { address.clone() },
amount: 100000 * (i + 1),
fee: 21000,
nonce: i,
timestamp: 1700000000 + (i * 600),
Copy link

Copilot AI Dec 9, 2025

Choose a reason for hiding this comment

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

[nitpick] Magic numbers: The timestamp base value 1700000000 appears multiple times without explanation. This represents a specific date (Nov 14, 2023) but it's not clear why this date was chosen for the mock data.

Consider:

  1. Defining it as a named constant with documentation
  2. Using a more current or clearly documented date
  3. Computing it from the current time minus an offset

Copilot uses AI. Check for mistakes.
Comment on lines +513 to +524
<div class="item" onclick="showBlockDetail(${block.height})">
<div class="item-header">
<span class="item-title">Block #${block.height}</span>
<span class="badge">${block.battle_count} Battle${block.battle_count !== 1 ? 's' : ''}</span>
</div>
<div class="item-meta">
<div class="hash">Hash: ${block.hash}</div>
<div>Proposer: ${block.proposer} • ${new Date(block.timestamp * 1000).toLocaleString()}</div>
<div>Transactions: ${block.transaction_count}</div>
</div>
</div>
`).join('');
Copy link

Copilot AI Dec 9, 2025

Choose a reason for hiding this comment

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

Accessibility issue: Interactive elements (blocks, transactions, accounts) that trigger onclick events don't have appropriate ARIA roles or keyboard navigation support. Users who rely on keyboard navigation cannot access these features.

Consider:

  1. Adding role="button" and tabindex="0" to clickable items
  2. Adding keyboard event handlers for Enter/Space keys
  3. Alternatively, use actual <button> elements with appropriate styling

Example:

<div class="item" role="button" tabindex="0" onclick="showBlockDetail(${block.height})" onkeypress="if(event.key==='Enter'||event.key===' ')showBlockDetail(${block.height})">

Copilot uses AI. Check for mistakes.
Comment on lines +656 to +660
<div class="detail-value hash" style="cursor: pointer;" onclick="showAccountDetail('${tx.from}')">${tx.from}</div>
</div>
<div class="detail-field">
<div class="detail-label">To</div>
<div class="detail-value hash" style="cursor: pointer;" onclick="showAccountDetail('${tx.to}')">${tx.to}</div>
Copy link

Copilot AI Dec 9, 2025

Choose a reason for hiding this comment

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

XSS vulnerability: User-controlled addresses from the tx.from and tx.to fields are directly injected into the onclick handler without sanitization. A malicious address could contain JavaScript code that would execute when clicked.

Consider using data attributes to store the address and reading it in the click handler:

<div class="detail-value hash" data-address="${tx.from}" onclick="showAccountDetail(this.dataset.address)">${escapeHtml(tx.from)}</div>

And add a sanitization function:

function escapeHtml(text) {
    const div = document.createElement('div');
    div.textContent = text;
    return div.innerHTML;
}

Copilot uses AI. Check for mistakes.
Comment on lines +205 to +227
// Check if it's a transaction hash (0x + 64 hex chars)
if q.starts_with("0x") && q.len() == 66 {
results.push(SearchResult::Transaction {
hash: q.to_string(),
});
}

// Check if it's a block hash (0x + 16 hex chars for simplified mock)
// Note: In production, block hashes would typically be 32 bytes (64 hex chars)
if q.starts_with("0x") && q.len() == 18 {
// Try to find block by hash
results.push(SearchResult::Block {
height: 12345, // Mock value
hash: q.to_string(),
});
}

// Check if it's an account address (0x + 40 hex chars)
if q.starts_with("0x") && q.len() == 42 {
results.push(SearchResult::Account {
address: q.to_string(),
});
}
Copy link

Copilot AI Dec 9, 2025

Choose a reason for hiding this comment

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

Missing hex validation: Validation is incomplete for addresses and transaction hashes. The checks at lines 206, 214, and 223 validate length but not whether the strings contain valid hexadecimal characters.

Add hex validation to all branches:

// For transaction hash:
if q.starts_with("0x") && q.len() == 66 && q[2..].chars().all(|c| c.is_ascii_hexdigit()) {
    results.push(SearchResult::Transaction {
        hash: q.to_string(),
    });
}

Copilot uses AI. Check for mistakes.
Comment on lines +205 to +220
// Check if it's a transaction hash (0x + 64 hex chars)
if q.starts_with("0x") && q.len() == 66 {
results.push(SearchResult::Transaction {
hash: q.to_string(),
});
}

// Check if it's a block hash (0x + 16 hex chars for simplified mock)
// Note: In production, block hashes would typically be 32 bytes (64 hex chars)
if q.starts_with("0x") && q.len() == 18 {
// Try to find block by hash
results.push(SearchResult::Block {
height: 12345, // Mock value
hash: q.to_string(),
});
}
Copy link

Copilot AI Dec 9, 2025

Choose a reason for hiding this comment

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

Inconsistent hash validation: The search function accepts block hashes of 18 characters (line 214) while transaction hashes require 66 characters (line 206). The documentation at line 12 states "typically 64 in production", which would be 66 characters with the "0x" prefix. This inconsistency is confusing.

Consider either:

  1. Using consistent 66-character hashes for both blocks and transactions
  2. Clearly documenting why block hashes are shorter in the mock implementation
  3. Making the validation match production expectations

Copilot uses AI. Check for mistakes.
Comment on lines +87 to +92
if !hash.starts_with("0x") || hash.len() != 66 {
return Err((
StatusCode::BAD_REQUEST,
Json("Invalid transaction hash format".to_string()),
));
}
Copy link

Copilot AI Dec 9, 2025

Choose a reason for hiding this comment

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

Missing hex validation: The validation checks the length but doesn't verify that the string contains only valid hexadecimal characters. A string like 0x followed by 64 non-hex characters would pass validation.

Add hex character validation:

if !hash.starts_with("0x") || hash.len() != 66 || !hash[2..].chars().all(|c| c.is_ascii_hexdigit()) {
    return Err((
        StatusCode::BAD_REQUEST,
        Json("Invalid transaction hash format".to_string()),
    ));
}

Copilot uses AI. Check for mistakes.
Comment on lines +116 to +121
if !address.starts_with("0x") || address.len() != 42 {
return Err((
StatusCode::BAD_REQUEST,
Json("Invalid address format".to_string()),
));
}
Copy link

Copilot AI Dec 9, 2025

Choose a reason for hiding this comment

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

Missing hex validation: Same issue as in get_transaction - the validation only checks length but not hexadecimal character validity. An address with invalid characters would pass validation.

Add hex character validation:

if !address.starts_with("0x") || address.len() != 42 || !address[2..].chars().all(|c| c.is_ascii_hexdigit()) {
    return Err((
        StatusCode::BAD_REQUEST,
        Json("Invalid address format".to_string()),
    ));
}

Copilot uses AI. Check for mistakes.
Comment on lines +489 to +495
function switchTab(tab) {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));

event.target.classList.add('active');
document.getElementById(tab + '-tab').classList.add('active');
}
Copy link

Copilot AI Dec 9, 2025

Choose a reason for hiding this comment

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

Missing parameter in function: The switchTab function uses event.target without declaring event as a parameter. This relies on the global event object which is non-standard and deprecated.

Fix by adding the event parameter:

function switchTab(tab, event) {
    document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
    document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
    
    event.target.classList.add('active');
    document.getElementById(tab + '-tab').classList.add('active');
}

And update the onclick calls:

<button class="tab active" onclick="switchTab('blocks', event)">Recent Blocks</button>

Copilot uses AI. Check for mistakes.
Comment thread docs/BLOCK_EXPLORER.md Outdated
### 🔍 Search Functionality
- **Universal Search Bar**: Search by:
- Block height (numeric)
- Block hash (0x + hex characters - currently 16 for mock, typically 64 in production)
Copy link

Copilot AI Dec 9, 2025

Choose a reason for hiding this comment

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

Inconsistent documentation: The documentation states block hashes are "0x + hex characters - currently 16 for mock, typically 64 in production", but the actual implementation generates 16 hex characters (18 total with "0x" prefix) in the mock data while searching for 18-character hashes. The discrepancy between "16" and the actual implementation is confusing.

Clarify to: "currently 16 hex characters (18 with 0x prefix) for mock"

Suggested change
- Block hash (0x + hex characters - currently 16 for mock, typically 64 in production)
- Block hash (0x + hex characters - currently 16 hex characters (18 with 0x prefix) for mock, typically 64 in production)

Copilot uses AI. Check for mistakes.
Comment on lines +116 to +250
if !address.starts_with("0x") || address.len() != 42 {
return Err((
StatusCode::BAD_REQUEST,
Json("Invalid address format".to_string()),
));
}

// Mock trust score calculation
let trust_score = 0.85;

Ok(Json(AccountInfo {
address: address.clone(),
balance: 5000000, // 5 CELL
nonce: 10,
transaction_count: 25,
trust_score,
is_miner: trust_score > 0.75,
}))
}

/// Get transaction history for an account
pub async fn get_account_transactions(
State(_state): State<Arc<AppState>>,
Path(address): Path<String>,
Query(pagination): Query<PaginationQuery>,
) -> Result<Json<TransactionHistoryResponse>, (StatusCode, Json<String>)> {
// Validate address format
if !address.starts_with("0x") || address.len() != 42 {
return Err((
StatusCode::BAD_REQUEST,
Json("Invalid address format".to_string()),
));
}

// In a real implementation, this would query the transaction index
// For now, return mock data
let mut transactions = Vec::new();

// Generate some mock transactions
for i in 0..10 {
transactions.push(TransactionDetail {
hash: format!("0x{:064x}", i * 12345),
block_height: 12340 + i,
from: if i % 2 == 0 { address.clone() } else { "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb8".to_string() },
to: if i % 2 == 0 { "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb8".to_string() } else { address.clone() },
amount: 100000 * (i + 1),
fee: 21000,
nonce: i,
timestamp: 1700000000 + (i * 600),
status: "confirmed".to_string(),
});
}

// Apply pagination
let start = (pagination.page - 1) * pagination.per_page;
let end = start + pagination.per_page;
let paginated = transactions.into_iter().skip(start).take(pagination.per_page).collect::<Vec<_>>();

Ok(Json(TransactionHistoryResponse {
transactions: paginated,
total: 10,
page: pagination.page,
per_page: pagination.per_page,
}))
}

/// Search for blocks, transactions, or accounts
pub async fn search(
State(_state): State<Arc<AppState>>,
Query(query): Query<SearchQuery>,
) -> Result<Json<SearchResponse>, (StatusCode, Json<String>)> {
let q = query.q.trim();
let mut results = Vec::new();

if q.is_empty() {
return Err((
StatusCode::BAD_REQUEST,
Json("Empty search query".to_string()),
));
}

// Check if it's a block height (numeric)
if let Ok(height) = q.parse::<u64>() {
results.push(SearchResult::Block {
height,
hash: format!("0x{:016x}", height * 12345),
});
}

// Check if it's a transaction hash (0x + 64 hex chars)
if q.starts_with("0x") && q.len() == 66 {
results.push(SearchResult::Transaction {
hash: q.to_string(),
});
}

// Check if it's a block hash (0x + 16 hex chars for simplified mock)
// Note: In production, block hashes would typically be 32 bytes (64 hex chars)
if q.starts_with("0x") && q.len() == 18 {
// Try to find block by hash
results.push(SearchResult::Block {
height: 12345, // Mock value
hash: q.to_string(),
});
}

// Check if it's an account address (0x + 40 hex chars)
if q.starts_with("0x") && q.len() == 42 {
results.push(SearchResult::Account {
address: q.to_string(),
});
}

if results.is_empty() {
return Err((
StatusCode::NOT_FOUND,
Json("No results found".to_string()),
));
}

Ok(Json(SearchResponse { results }))
}

/// Get trust score for an account
pub async fn get_trust_score(
State(_state): State<Arc<AppState>>,
Path(address): Path<String>,
) -> Result<Json<TrustScoreResponse>, (StatusCode, Json<String>)> {
// Validate address format
if !address.starts_with("0x") || address.len() != 42 {
return Err((
StatusCode::BAD_REQUEST,
Json("Invalid address format".to_string()),
));
}
Copy link

Copilot AI Dec 9, 2025

Choose a reason for hiding this comment

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

Code duplication: Address validation logic is duplicated across four functions (get_account, get_account_transactions, get_trust_score, and within the search function). This violates DRY principle and makes maintenance harder.

Consider extracting to a helper function:

fn validate_address(address: &str) -> Result<(), (StatusCode, Json<String>)> {
    if !address.starts_with("0x") || address.len() != 42 || !address[2..].chars().all(|c| c.is_ascii_hexdigit()) {
        return Err((
            StatusCode::BAD_REQUEST,
            Json("Invalid address format".to_string()),
        ));
    }
    Ok(())
}

Then call it: validate_address(&address)?;

Copilot uses AI. Check for mistakes.
@Steake
Copy link
Copy Markdown
Owner

Steake commented Dec 9, 2025

@copilot apply changes based on the comments in this thread

Please re-factor this to use a proper web framework. There should be no dummy data, there should be no mocked data. We should be using real API calls, or real rpc calls.

The use of react is explicitly banned. Please use svelte/sveltekit. If there is no need for SSR, then don't use it.

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Dec 9, 2025

@copilot apply changes based on the comments in this thread

Please re-factor this to use a proper web framework. There sho...

I'll refactor the block explorer to use SvelteKit with real RPC calls to the BitCell node. This will involve:

  1. Creating a new SvelteKit frontend app in crates/bitcell-explorer/
  2. Removing the mock API endpoints and inline HTML from bitcell-admin
  3. Connecting to the existing RPC endpoints in bitcell-node (eth_getBlockByNumber, bitcell_getTournamentState, etc.)
  4. Addressing the security and accessibility issues from the code review

The explorer will be a standalone SPA that connects to node RPC endpoints. Should I proceed with this approach?

@copilot apply changes based on the comments in this thread

Please re-factor this to use a proper web framework. There sho...

Refactor complete in commit 8cd0284.

The block explorer is now a standalone SvelteKit application that:

  • ✅ Uses real RPC calls to BitCell node (no mock data)
  • ✅ Client-side SPA (no SSR)
  • ✅ Built with Svelte (React banned as requested)
  • ✅ Proper input validation and XSS protection
  • ✅ Accessibility improvements

Location: crates/bitcell-explorer/

To run:

cd crates/bitcell-explorer
npm install
npm run dev

- Remove mock data API endpoints from bitcell-admin
- Remove inline HTML/JS explorer
- Create new SvelteKit application in crates/bitcell-explorer/
- Connect to real BitCell node RPC endpoints
- No SSR, client-side only SPA
- Proper input validation and XSS protection
- Accessibility improvements (ARIA labels, keyboard nav)
- Updated documentation

Addresses code review comments:
- Removed all mock/dummy data
- Added hex validation for addresses and hashes
- Fixed XSS vulnerabilities
- Added proper accessibility features
- Using modern web framework (Svelte, not React)

Co-authored-by: Steake <530040+Steake@users.noreply.github.com>
Copilot AI changed the title Implement block explorer with tournament visualization and trust score tracking SvelteKit block explorer with real RPC integration Dec 9, 2025
@Steake Steake merged commit 4fa6238 into master Dec 11, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Build Block Explorer with Tournament Visualization

3 participants