explorer: native contract source code UI

This commit introduces functionality for displaying native contract source code in the DarkFi explorer application, enabling users to view available contracts from the home page, access associated source files, and examine native contract Rust code and zk proofs. This enhancement provides accessibility to native contract information. To support these updates, we have added new pages to the UI, updated styles for rendering source-related pages, and implemented new RPC routes to facilitate contract-related code retrieval.

Additionally, we have restructured the application into Flask blueprints to address the growing complexity of the explorer and adhere to best practices, as app.py was becoming large with diverging functionality. This proactive change prepares us for future expansions, including better organization of graph functionalities. The organization also mirrors the rest of the explorer codebase by maintaining separate pages for contracts, transactions, and blocks, as well as corresponding RPC and service rust modules.

UI Support for Native Contract Source Code

- We have added source code functionality to the UI, Flask blueprints, and RPC routes defined in rpc.py, along with updated styles to effectively render source-related pages.

Style Updates:
- Expanded the color palette for rendering source files.
- Enhanced styles for displaying contract IDs and names in the native contracts table.
- Implemented layout elements for proper display of source code lists and content.
- Added styles for source file list links, including hover effects.

RPC Routes:
- New routes have been added to facilitate requests to the DarkFi daemon for retrieving native contracts, source code paths, and contract source.

UI Enhancements:
- A native contract section has been introduced on the explorer home page to display native contracts.
- A contract source list page allows users to view and click on source code files associated with a native contract.
- A dedicated contract source page renders the source for a specific path associated with a given contract.

Introducing Flask Blueprints

To better manage the growing complexity of the application, we introduced Flask blueprints that organize routes according to their functionality. This restructuring not only enhances readability but also aligns with best practices for Flask applications, facilitating future development.

App.py Updates:
- Routes have been moved to their respective blueprints.

Added Blueprints:
- `__init__.py` file initializes the explorer blueprint package and exposes various blueprints used by the application.
- `block.py` module defines a blueprint for handling block-related functionality.
- `contract.py` module defines a blueprint for managing contract-related functionality.
- explore.py` module manages general functionality related to the home page and search features.
- `transaction.py` module handles transaction-related functionality.

Python Documentation:

- We documented the Python code and updated existing documentation to adhere to Python's best practices for using docstrings, enhancing usability and integration with documentation tools.

Testing:

- Manual testing has been conducted through the UI to verify the functionality of the new RPC methods, ensuring they correctly retrieve and display contract-related information.
This commit is contained in:
kalm
2024-12-25 08:35:34 -08:00
parent 900b3eeb30
commit c58fd56041
12 changed files with 634 additions and 64 deletions

View File

@@ -15,63 +15,62 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import rpc
from flask import Flask, request, render_template
"""
Module: app.py
app = Flask(__name__)
This module initializes the DarkFi explorer Flask application by registering various blueprints for handling routes
related to blocks, contracts, transactions, search, and the explore section, including the home page. It also defines
error handlers, ensuring appropriate responses for these common HTTP errors.
"""
@app.route('/')
async def index():
# Fetch data from RPC
blocks = await rpc.get_last_n_blocks("10")
basic_stats = await rpc.get_basic_statistics()
from flask import Flask, render_template
# Fetch the metric statistics
metric_stats = await rpc.get_metric_statistics()
has_metrics = metric_stats and isinstance(metric_stats, list)
from blueprints.explore import explore_bp
from blueprints.block import block_bp
from blueprints.contract import contract_bp
from blueprints.transaction import transaction_bp
# Get the latest metric statistics, or use None if no metrics are found
if has_metrics:
latest_metric_stats = metric_stats[-1]
else:
latest_metric_stats = None
def create_app():
"""
Creates and configures the DarkFi explorer Flask application.
# Render template
return render_template(
'index.html',
blocks=blocks,
basic_stats=basic_stats,
metric_stats=latest_metric_stats,
)
This function creates and initializes the explorer the Flask app,
registering applicable blueprints for handling explorer-related routes,
and defining error handling for common HTTP errors. It returns a fully
configured Flask application instance.
"""
app = Flask(__name__)
@app.route('/search', methods=['GET', 'POST'])
async def search():
search_hash = request.args.get('search_hash', '')
block = await rpc.get_block(search_hash)
transactions = await rpc.get_block_transactions(search_hash)
if transactions:
return render_template('block.html', block=block, transactions=transactions)
else:
transaction = await rpc.get_transaction(search_hash)
return render_template('transaction.html', transaction=transaction)
# Register Blueprints
app.register_blueprint(explore_bp)
app.register_blueprint(block_bp)
app.register_blueprint(contract_bp)
app.register_blueprint(transaction_bp)
@app.route('/block/<header_hash>')
async def block(header_hash):
block = await rpc.get_block(header_hash)
transactions = await rpc.get_block_transactions(header_hash)
return render_template('block.html', block=block, transactions=transactions)
# Define page not found error handler
@app.errorhandler(404)
def page_not_found(e):
"""
Handles 404 errors by rendering a custom 404 error page when a requested page is not found,
returning a rendered template along with a 404 status code.
Args:
e: The error object associated with the 404 error.
"""
# Render the custom 404 error page
return render_template('404.html'), 404
@app.route('/transaction/<transaction_hash>')
async def transaction(transaction_hash):
transaction = await rpc.get_transaction(transaction_hash)
return render_template('transaction.html', transaction=transaction)
# Define internal server error handler
@app.errorhandler(500)
def internal_server_error(e):
"""
Handles 500 errors by rendering a custom 500 error page when an internal server error occurs,
returning a rendered template along with a 500 status code.
@app.errorhandler(404)
def page_not_found(e):
return render_template('404.html'), 404
@app.errorhandler(500)
def page_not_found(e):
return render_template('500.html'), 500
Args:
e: The error object associated with the 500 error.
"""
# Render the custom 500 error page
return render_template('500.html'), 500
return app

View File

@@ -0,0 +1,36 @@
# This file is part of DarkFi (https://dark.fi)
#
# Copyright (C) 2020-2024 Dyne.org foundation
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
Explorer blueprint package initializer.
This module imports and exposes blueprints for used by the explorer
flask application.
Exposed Blueprints:
- explore_bp: The explorer application blueprint.
- block_bp: The blueprint for block-related functionality.
- contract_bp: The blueprint for contract-related functionality.
- transaction_bp: The blueprint for transaction-related functionality.
"""
from .explore import explore_bp
from .block import block_bp
from .contract import contract_bp
from .transaction import transaction_bp
# Expose blueprints for importing
__all__ = ["explore_bp", "block_bp", "contract_bp", "transaction_bp"]

View File

@@ -0,0 +1,48 @@
# This file is part of DarkFi (https://dark.fi)
#
# Copyright (C) 2020-2024 Dyne.org foundation
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
Blueprint: block_bp
This module defines a Flask blueprint (`block_bp`) for handling block-related functionality,
serving as a primary location for Flask code related to routes and features associated with blocks.
"""
from flask import Blueprint, render_template
import rpc
# Create block blueprint
block_bp = Blueprint("block", __name__)
@block_bp.route('/block/<header_hash>')
async def block(header_hash):
"""
Retrieves and displays details of a specific block and its associated transactions based
on the provided header hash using RPC calls to the explorer daemon, returning a rendered template.
Path Args:
header_hash (str): The header hash of the block to retrieve.
"""
# Fetch the block details
block = await rpc.get_block(header_hash)
# Fetch transactions associated with the block
transactions = await rpc.get_block_transactions(header_hash)
# Render the template with the block details and associated transactions
return render_template('block.html', block=block, transactions=transactions)

View File

@@ -0,0 +1,94 @@
# This file is part of DarkFi (https://dark.fi)
#
# Copyright (C) 2020-2024 Dyne.org foundation
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
Blueprint: contract_bp
This module defines a Flask blueprint (`contract_bp`) for handling contract-related functionality,
serving as a primary location for Flask code related to routes and related features associated with contracts.
"""
from flask import request, render_template, Blueprint
from pygments import highlight
from pygments.lexers import RustLexer
from pygments.formatters import HtmlFormatter
import rpc
# Create contract blueprint
contract_bp = Blueprint("contract", __name__)
@contract_bp.route('/contract/<contract_id>')
async def contract_source_list(contract_id):
"""
Fetches and displays a list of source files for the specified contract using an RPC
call to the explorer daemon, returning a rendered template with the source code
files associated with the contract.
Args:
contract_id (str): The Contract ID to fetch the source files for.
Query Params:
name (str, optional): The contract name to display alongside the source files.
"""
# Obtain the contract name to display with the source
contract_name = request.args.get('name')
# Fetch the source file list associated with the contract
source_paths = await rpc.get_contract_source_paths(contract_id)
# Returned rendered contract source list
return render_template('contract_source_list.html', contract_id=contract_id, source_paths=source_paths, contract_name=contract_name)
@contract_bp.route('/contract/source/<contract_id>/<path:source_path>')
async def contract_source(contract_id, source_path):
"""
Fetches and displays the source code for a specific file of a contract using an RPC
call to the explorer daemon, returning a rendered template with syntax-highlighted
source code.
Path Args:
contract_id (str): The Contract ID to fetch the source file for.
source_path (str): The path of the specific source file within the contract.
Query Params:
name (str, optional): The contract name to display alongside the source code.
"""
# Obtain the contract name to display with the source
contract_name = request.args.get('name')
# Retrieve the contract source code
raw_source = await rpc.get_contract_source(contract_id, source_path)
# Style the source code
formatter = HtmlFormatter(style='friendly', linenos=True)
source = highlight(raw_source, RustLexer(), formatter)
# Generate css for styled source code
pygments_css = formatter.get_style_defs()
# Returned rendered contract source code page
return render_template(
'contract_source.html',
source=source,
contract_id=contract_id,
source_path=source_path,
pygments_css=pygments_css,
contract_name=contract_name
)

View File

@@ -0,0 +1,94 @@
# This file is part of DarkFi (https://dark.fi)
#
# Copyright (C) 2020-2024 Dyne.org foundation
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
Blueprint: explorer_bp
This module defines a Flask blueprint (`explore_bp`) for managing the general functionality of the explorer application.
It serves as the primary location for Flask routes related to the home page, search functionality, and other general features.
"""
from flask import Blueprint, render_template, request
import rpc
# Create explore blueprint
explore_bp = Blueprint("explore", __name__)
@explore_bp.route('/', methods=["GET"])
async def index():
"""
Fetches and displays the explorer home page content using multiple RPC calls to the
explorer daemon, retrieving the last 10 blocks, basic statistics, metric statistics,
and DarkFi native contracts.
Upon success, it returns a rendered template with recent blocks, basic statistics,
latest metric statistics (if available), and native contracts.
"""
# Fetch the latest 10 blocks
blocks = await rpc.get_last_n_blocks("10")
# Retrieve basic statistics summarizing the overall chain data
basic_stats = await rpc.get_basic_statistics()
# Fetch the metric statistics
metric_stats = await rpc.get_metric_statistics()
# Determine if metrics exist
has_metrics = metric_stats and isinstance(metric_stats, list)
# Get the latest metric statistics or set to None if none are found
latest_metric_stats = metric_stats[-1] if has_metrics else None
# Retrieve the native contracts
native_contracts = await rpc.get_native_contracts()
# Render the explorer home page
return render_template(
'index.html',
blocks=blocks,
basic_stats=basic_stats,
metric_stats=latest_metric_stats,
native_contracts=native_contracts,
)
@explore_bp.route('/search', methods=['GET', 'POST'])
async def search():
"""
Searches for a block or transaction based on the provided hash using RPC calls to the
explorer daemon. It retrieves relevant data using the search hash from provided query parameter,
returning a rendered template that displays either block details with associated transactions
or transaction details, depending on the search result.
Query Params:
search_hash (str): The hash of the block or transaction to search for.
"""
# Get the search hash
search_hash = request.args.get('search_hash', '')
# Fetch the block corresponding to the search hash
block = await rpc.get_block(search_hash)
# Fetch transactions associated with the block
transactions = await rpc.get_block_transactions(search_hash)
if transactions:
# Render block details with associated transactions if found
return render_template('block.html', block=block, transactions=transactions)
else:
# Fetch transaction details if no transactions are found for the block
transaction = await rpc.get_transaction(search_hash)
return render_template('transaction.html', transaction=transaction)

View File

@@ -0,0 +1,45 @@
# This file is part of DarkFi (https://dark.fi)
#
# Copyright (C) 2020-2024 Dyne.org foundation
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
Blueprint: transaction_bp
This module defines a Flask blueprint (`transaction_bp`) for handling transaction-related functionality,
serving as a primary location for Flask code related to routes and related features associated with transactions.
"""
from flask import Blueprint, render_template
import rpc
# Create transaction blueprint
transaction_bp = Blueprint("transaction", __name__)
@transaction_bp.route('/tx/<transaction_hash>')
async def transaction(transaction_hash):
"""
Retrieves transaction details based on the provided hash using RPC calls to the explorer daemon
and returns a rendered template displaying the information.
Path Args:
transaction_hash (str): The hash of the transaction to retrieve.
"""
# Fetch the transaction details
transaction = await rpc.get_transaction(transaction_hash)
# Render the template using the fetched transaction details
return render_template('transaction.html', transaction=transaction)

View File

@@ -1 +1,2 @@
flask[async]
Pygments

View File

@@ -15,27 +15,38 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
Module: rpc.py
This module provides an asynchronous interface for interacting with the DarkFi explorer daemon
using JSON-RPC. It includes functionality to create a communication channel, send requests,
and handle responses from the server.
"""
import asyncio, json, random
from flask import abort
# DarkFi blockchain-explorer daemon JSON-RPC configuration
# DarkFi explorer daemon JSON-RPC configuration
URL = "127.0.0.1"
PORT = 14567
# Class representing the channel with the JSON-RPC server
class Channel:
"""Class representing the channel with the JSON-RPC server."""
def __init__(self, reader, writer):
"""Initialize the channel with a reader and writer."""
self.reader = reader
self.writer = writer
async def readline(self):
"""Read a line from the channel, closing it if the connection is lost."""
if not (line := await self.reader.readline()):
self.writer.close()
return None
# Strip the newline
return line[:-1].decode()
return line[:-1].decode() # Strip the newline
async def receive(self):
"""Receive and decode a message from the channel."""
if (plaintext := await self.readline()) is None:
return None
@@ -44,34 +55,47 @@ class Channel:
return response
async def send(self, obj):
"""Send a JSON-encoded object to the channel."""
message = json.dumps(obj)
data = message.encode()
self.writer.write(data + b"\n")
await self.writer.drain()
# Create a Channel for given server.
async def create_channel(server_name, port):
"""
Creates a channel used to send RPC requests to the DarkFi explorer daemon.
"""
try:
reader, writer = await asyncio.open_connection(server_name, port)
except ConnectionRefusedError:
print(f"Error: Connection Refused to '{server_name}:{port}', Either because the daemon is down, is currently syncing or wrong url.")
print(
f"Error: Connection Refused to '{server_name}:{port}', Either because the daemon is down, is currently syncing or wrong url.")
abort(500)
channel = Channel(reader, writer)
return channel
# Execute a request towards the JSON-RPC server
async def query(method, params):
"""
Execute a request towards the JSON-RPC server by constructing a JSON-RPC
request and sending it to the server. It handles connection errors and server responses,
returning the result of the query or raising an error if the request fails.
"""
# Create the channel to send RPC request
channel = await create_channel(URL, PORT)
# Prepare request
request = {
"id": random.randint(0, 2**32),
"id": random.randint(0, 2 ** 32),
"method": method,
"params": params,
"jsonrpc": "2.0",
}
await channel.send(request)
# Send request and await response
await channel.send(request)
response = await channel.receive()
# Closed connect returns None
if response is None:
print("error: connection with server was closed")
@@ -86,26 +110,40 @@ async def query(method, params):
return response["result"]
# Retrieve last n blocks from blockchain-explorer daemon
async def get_last_n_blocks(n: str):
"""Retrieves the last n blocks."""
return await query("blocks.get_last_n_blocks", [n])
# Retrieve basic statistics from blockchain-explorer daemon
async def get_basic_statistics():
"""Retrieves basic statistics."""
return await query("statistics.get_basic_statistics", [])
# Retrieve fee data statistics from blockchain-explorer daemon
async def get_metric_statistics():
"""Retrieves metrics statistics."""
return await query("statistics.get_metric_statistics", [])
# Retrieve the block information of given header hash from blockchain-explorer daemon
async def get_block(header_hash: str):
"""Retrieves block information for a given header hash."""
return await query("blocks.get_block_by_hash", [header_hash])
# Retrieve the transactions of given block header hash from blockchain-explorer daemon
async def get_block_transactions(header_hash: str):
"""Retrieves transactions associated with a given block header hash."""
return await query("transactions.get_transactions_by_header_hash", [header_hash])
# Retrieve the transaction information of given hash from blockchain-explorer daemon
async def get_transaction(transaction_hash: str):
"""Retrieves transaction information for a given transaction hash."""
return await query("transactions.get_transaction_by_hash", [transaction_hash])
async def get_native_contracts():
"""Retrieves native contracts."""
return await query("contracts.get_native_contracts", [])
async def get_contract_source_paths(contract_id: str):
"""Retrieves contract source code paths for a given contract ID."""
return await query("contracts.get_contract_source_code_paths", [contract_id])
async def get_contract_source(contract_id: str, source_path):
"""Retrieves the contract source file for a given contract ID and source path."""
return await query("contracts.get_contract_source", [contract_id, source_path])

View File

@@ -115,6 +115,9 @@
--color-background-table-header: #181818;
--color-input-primary: #AFA99E;
--color-border-primary: #83796B;
--color-source-code-link: #374151;
--color-source-code-link-bg: #393939;
--color-source-code-link-hover-bg: #313131;
}
/* -------------------------------------------------------------------------- */
@@ -974,3 +977,89 @@ h5 a:hover {
.table thead tr {
border: 1px var(--color-border-primary) solid;
}
/* -------------------------------------------------------------------------- */
/* Native Contracts */
/* -------------------------------------------------------------------------- */
.contract-id {
width: calc(27ch + 12px);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.contract-name {
width: calc(9ch + 12px);
padding: 6px;
}
.contract-description {
width: auto;
}
/* -------------------------------------------------------------------------- */
/* Source Code */
/* -------------------------------------------------------------------------- */
.source-code-container {
display: flex;
flex-direction: column;
}
.source-code-display-box {
padding: 16px;
border: 1px var(--color-border-primary) solid;
overflow: hidden;
box-sizing: border-box;
}
.source-path {
align-self: flex-start;
margin-bottom: 12px;
margin-right: 16px;
}
.source-code-list {
list-style-type: none;
padding-left: 0;
}
.source-code-list-item {
margin-bottom: 8px;
}
.source-file-link {
color: var(--color-source-code-link);
text-decoration: none;
font-weight: bold;
display: block;
padding: 8px 12px;
background-color: var(--color-source-code-link-bg);
border-radius: 2px;
transition: background-color 0.3s ease;
}
.source-file-link:hover {
background-color: var(--color-source-code-link-hover-bg);
color: var(--color-source-code-link);
}
.pre-source {
overflow: hidden;
white-space: pre-wrap;
word-wrap: break-word;
margin: 0;
padding: 0;
line-height: 1;
border: none;
}
.pre-source table {
width: 100%;
border-collapse: collapse;
}
.pre-source td {
padding-right: 2px;
}

View File

@@ -0,0 +1,46 @@
<!--
This file is part of DarkFi (https://dark.fi)
Copyright (C) 2020-2024 Dyne.org foundation
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
{% extends "base.html" %}
{% block title %}Contract {{ contract_id }}{% endblock %}
{% block content %}
<div class="content-container">
<!-- Page Content Header -->
<div class="content-header">
<h2 class="content-section-header">{{ contract_name }} Contract Source Code</h2>
<div class="">{{ contract_id }}</div>
</div>
<!-- Source Code -->
<div class="source-code-container">
<div class="source-path">
{{ source_path }}
</div>
<div class="source-code-display-box">
<!--Include Pygments CSS for syntax highlighting-->
<style>{{ pygments_css|safe }}</style>
<!-- Display the highlighted code -->
<pre class="pre-source">{{ source|safe }}</pre>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,55 @@
<!--
This file is part of DarkFi (https://dark.fi)
Copyright (C) 2020-2024 Dyne.org foundation
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
{% extends "base.html" %}
{% block title %}Contract {{ contract_id }}{% endblock %}
{% block content %}
<div class="content-container">
<!-- Page Content Header -->
<div class="content-header">
<h2 class="content-section-header">{{ contract_name }} Contract Source Code</h2>
<div>{{ contract_id }}</div>
</div>
<!-- Source File List -->
<div>
{% if source_paths %}
<!-- Source Code List -->
<ul class="source-code-list">
{% for path in source_paths %}
<li class="source-code-list-item">
<a href="/contract/source/{{ contract_id }}/{{ path }}?name={{ contract_name|urlencode }}"
class="source-file-link">
{{ path }}
</a>
</li>
{% endfor %}
</ul>
{% else %}
<!-- No source code associated with contract -->
<div>
There is no source files are associated with this contract.
</div>
{% endif %}
</div>
</div>
{% endblock %}

View File

@@ -117,6 +117,31 @@
{% endif %}
</div>
<!--Native Contracts -->
<div class="content-section">
<h2 class="content-section-header">Native Contracts</h2>
<table class="table">
<thead>
<tr class="table-header">
<th class="contract-id">Contract Id</th>
<th class="contract-name text-center">Name</th>
<th class="contract-description">Description</th>
</tr>
</thead>
<tbody>
{% for contract in native_contracts %}
<tr class="table-row">
<td class="contract-id">
<a href="contract/{{ contract[0] }}?name={{ contract[1]|urlencode }}">{{ contract[0] }}</a>
</td>
<td class="contract-name text-center">{{ contract[1] }}</td>
<td class="contract-description">{{ contract[2] }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}