mirror of
https://github.com/darkrenaissance/darkfi.git
synced 2026-01-09 14:48:08 -05:00
explorer: move explorer source code from research to bin/explorer for project releases
This commit relocates the explorer code from the research directory to `bin/explorer` to include it as part of future releases. ### Summary of Updates: #### Darkfi Project - Updated `Cargo.toml` to include `bin/explorer/explorerd` as a workspace member - Updated `Cargo.lock` to include the `explorerd` package - Updated the `Makefile` to include `explorerd` in the build process #### Explorer Daemon - Renamed the project directory from `blockchain-explorer` to `explorer` - Moved the explorer daemon source code to `bin/explorer/explorerd` - Updated the cargo package name to `explorerd` - Updated log statement targets from `blockchain-explorer::` to `explorerd::` - Renamed the explorer configuration file to `explorerd_config.toml` - Removed Halo2 patches as they are now included in the root package - Changed default db_path to use explorerd instead of blockchain-explorer in the path - Changed binary crate Arg structopt name from blockchain-explorer to explorerd #### Explorer Site - Moved the explorer site source code to `bin/explorer/site` - Updated README.md to include new build instructions for explorerd
This commit is contained in:
@@ -1,5 +0,0 @@
|
||||
/target
|
||||
Cargo.lock
|
||||
rustfmt.toml
|
||||
*_db
|
||||
/native_contracts_src
|
||||
@@ -1,49 +0,0 @@
|
||||
[package]
|
||||
name = "blockchain-explorer"
|
||||
version = "0.4.1"
|
||||
description = "Daemon to listen for new blocks from darkfid and store them in an easily accessible format for further usage."
|
||||
authors = ["Dyne.org foundation <foundation@dyne.org>"]
|
||||
repository = "https://codeberg.org/darkrenaissance/darkfi"
|
||||
license = "AGPL-3.0-only"
|
||||
edition = "2021"
|
||||
|
||||
[workspace]
|
||||
|
||||
[dependencies]
|
||||
# Darkfi
|
||||
darkfi = {path = "../../../", features = ["async-daemonize", "validator"]}
|
||||
darkfi-sdk = {path = "../../../src/sdk"}
|
||||
darkfi-serial = "0.4.2"
|
||||
drk = {path = "../../../bin/drk"}
|
||||
|
||||
# JSON-RPC
|
||||
async-trait = "0.1.86"
|
||||
tinyjson = "2.5.1"
|
||||
url = "2.5.4"
|
||||
|
||||
# Daemon
|
||||
easy-parallel = "3.3.1"
|
||||
signal-hook-async-std = "0.2.2"
|
||||
signal-hook = "0.3.17"
|
||||
simplelog = "0.12.2"
|
||||
smol = "2.0.2"
|
||||
|
||||
# Argument parsing
|
||||
serde = {version = "1.0.217", features = ["derive"]}
|
||||
structopt = "0.3.26"
|
||||
structopt-toml = "0.5.1"
|
||||
|
||||
# Database
|
||||
sled-overlay = "0.1.6"
|
||||
|
||||
# Misc
|
||||
log = "0.4.25"
|
||||
lazy_static = "1.5.0"
|
||||
tar = "0.4.43"
|
||||
|
||||
# Testing
|
||||
tempdir = "0.3.7"
|
||||
|
||||
[patch.crates-io]
|
||||
halo2_proofs = {git="https://github.com/parazyd/halo2", branch="v4"}
|
||||
halo2_gadgets = {git="https://github.com/parazyd/halo2", branch="v4"}
|
||||
@@ -1,53 +0,0 @@
|
||||
.POSIX:
|
||||
|
||||
# Install prefix
|
||||
PREFIX = $(HOME)/.cargo
|
||||
|
||||
# Cargo binary
|
||||
CARGO = cargo +nightly
|
||||
|
||||
# Compile target
|
||||
RUST_TARGET = $(shell rustc -Vv | grep '^host: ' | cut -d' ' -f2)
|
||||
# Uncomment when doing musl static builds
|
||||
#RUSTFLAGS = -C target-feature=+crt-static -C link-self-contained=yes
|
||||
|
||||
SRC = \
|
||||
Cargo.toml \
|
||||
../../../Cargo.toml \
|
||||
$(shell find src -type f -name '*.rs') \
|
||||
$(shell find ../../../src -type f -name '*.rs') \
|
||||
|
||||
BIN = $(shell grep '^name = ' Cargo.toml | sed 1q | cut -d' ' -f3 | tr -d '"')
|
||||
|
||||
all: $(BIN) bundle_contracts_src
|
||||
|
||||
$(BIN): $(SRC)
|
||||
RUSTFLAGS="$(RUSTFLAGS)" $(CARGO) build --target=$(RUST_TARGET) --release --package $@
|
||||
# TODO: Update target path to darkfi project workspace once explorer is transitioned to bin
|
||||
cp -f target/$(RUST_TARGET)/release/$@ $@
|
||||
cp -f target/$(RUST_TARGET)/release/$@ ../../../$@
|
||||
|
||||
clean:
|
||||
RUSTFLAGS="$(RUSTFLAGS)" $(CARGO) clean --target=$(RUST_TARGET) --release --package $(BIN)
|
||||
rm -f $(BIN) ../../../$(BIN)
|
||||
rm -rf native_contracts_src
|
||||
|
||||
install: all
|
||||
mkdir -p $(DESTDIR)$(PREFIX)/bin
|
||||
cp -f $(BIN) $(DESTDIR)$(PREFIX)/bin
|
||||
chmod 755 $(DESTDIR)$(PREFIX)/bin/$(BIN)
|
||||
|
||||
uninstall:
|
||||
rm -f $(DESTDIR)$(PREFIX)/bin/$(BIN)
|
||||
|
||||
bundle_contracts_src:
|
||||
@mkdir -p $(CURDIR)/native_contracts_src
|
||||
@(cd ../../../src && \
|
||||
tar -cf $(CURDIR)/native_contracts_src/deployooor_contract_src.tar -C contract/deployooor/src --transform 's,^./,,' . && \
|
||||
tar -cf $(CURDIR)/native_contracts_src/dao_contract_src.tar -C contract/dao/src --transform 's,^./,,' . 2>/dev/null && \
|
||||
find contract/dao/proof -name '*.zk' -exec tar -rf $(CURDIR)/native_contracts_src/dao_contract_src.tar --transform 's,^.*proof/,proof/,' {} + && \
|
||||
tar -cf $(CURDIR)/native_contracts_src/money_contract_src.tar -C contract/money/src --transform 's,^./,,' . && \
|
||||
find contract/money/proof -name '*.zk' -exec tar -rf $(CURDIR)/native_contracts_src/money_contract_src.tar --transform 's,^.*proof/,proof/,' {} + \
|
||||
)
|
||||
|
||||
.PHONY: all clean install uninstall bundle_contracts_src
|
||||
@@ -1,19 +0,0 @@
|
||||
## blockchain-explorer configuration file
|
||||
##
|
||||
## Please make sure you go through all the settings so you can configure
|
||||
## your daemon properly.
|
||||
##
|
||||
## The default values are left commented. They can be overridden either by
|
||||
## uncommenting, or by using the command-line.
|
||||
|
||||
# JSON-RPC listen URL
|
||||
rpc_listen = "tcp://127.0.0.1:14567"
|
||||
|
||||
# Path to daemon database
|
||||
db_path = "~/.local/share/darkfi/blockchain-explorer/daemon.db"
|
||||
|
||||
# Password for the daemon database
|
||||
db_pass = "changeme"
|
||||
|
||||
# darkfid JSON-RPC endpoint
|
||||
endpoint = "tcp://127.0.0.1:8340"
|
||||
@@ -1,2 +0,0 @@
|
||||
/venv
|
||||
/__pycache__
|
||||
@@ -1,45 +0,0 @@
|
||||
Blockchain explorer web front-end
|
||||
=======
|
||||
|
||||
This is a very basic python based web front-end, based on `flask`,
|
||||
to serve static pages with blockchain data.
|
||||
|
||||
## Usage
|
||||
|
||||
We fist have to run 2 other daemons, to retrieve data from.
|
||||
Note: all paths are from repo root.
|
||||
|
||||
First we start a `darkfid` localnet:
|
||||
|
||||
```
|
||||
% cd contrib/localnet/darkfid-single-node/
|
||||
% ./tmux_sessions.sh
|
||||
```
|
||||
|
||||
It is advised to shutdown the `minerd` daemon after couple of blocks, to not waste resources.
|
||||
|
||||
Update the `blockchain-explorer` configuration to the localnet `darkfid` JSON-RPC endpoint
|
||||
and start a the daemon:
|
||||
|
||||
```
|
||||
% cd script/research/blockchain-explorer
|
||||
% cargo +nightly run --release --all-features
|
||||
```
|
||||
|
||||
Then we enter the site folder and we generate a new python virtual environment,
|
||||
source it and install required dependencies:
|
||||
|
||||
```
|
||||
% cd script/research/blockchain-explorer/site
|
||||
% python -m venv venv
|
||||
% source venv/bin/activate
|
||||
% pip install -r requirements.txt
|
||||
```
|
||||
|
||||
To start the `flask` server, simply execute:
|
||||
|
||||
```
|
||||
% python -m flask run
|
||||
```
|
||||
|
||||
The web site will be available at `127.0.0.1:5000`.
|
||||
@@ -1,76 +0,0 @@
|
||||
# This file is part of DarkFi (https://dark.fi)
|
||||
#
|
||||
# Copyright (C) 2020-2025 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/>.
|
||||
|
||||
"""
|
||||
Module: app.py
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
from flask import Flask, render_template
|
||||
|
||||
from blueprints.explore import explore_bp
|
||||
from blueprints.block import block_bp
|
||||
from blueprints.contract import contract_bp
|
||||
from blueprints.transaction import transaction_bp
|
||||
|
||||
def create_app():
|
||||
"""
|
||||
Creates and configures the DarkFi explorer Flask application.
|
||||
|
||||
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__)
|
||||
|
||||
# Register Blueprints
|
||||
app.register_blueprint(explore_bp)
|
||||
app.register_blueprint(block_bp)
|
||||
app.register_blueprint(contract_bp)
|
||||
app.register_blueprint(transaction_bp)
|
||||
|
||||
# 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
|
||||
|
||||
# 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.
|
||||
|
||||
Args:
|
||||
e: The error object associated with the 500 error.
|
||||
"""
|
||||
# Render the custom 500 error page
|
||||
return render_template('500.html'), 500
|
||||
|
||||
return app
|
||||
@@ -1,36 +0,0 @@
|
||||
# This file is part of DarkFi (https://dark.fi)
|
||||
#
|
||||
# Copyright (C) 2020-2025 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"]
|
||||
@@ -1,48 +0,0 @@
|
||||
# This file is part of DarkFi (https://dark.fi)
|
||||
#
|
||||
# Copyright (C) 2020-2025 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)
|
||||
@@ -1,94 +0,0 @@
|
||||
# This file is part of DarkFi (https://dark.fi)
|
||||
#
|
||||
# Copyright (C) 2020-2025 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
|
||||
)
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
# This file is part of DarkFi (https://dark.fi)
|
||||
#
|
||||
# Copyright (C) 2020-2025 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 return empty metrics
|
||||
latest_metric_stats = metric_stats[-1] if has_metrics else [0] * 15
|
||||
|
||||
# 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)
|
||||
@@ -1,45 +0,0 @@
|
||||
# This file is part of DarkFi (https://dark.fi)
|
||||
#
|
||||
# Copyright (C) 2020-2025 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)
|
||||
@@ -1,2 +0,0 @@
|
||||
flask[async]
|
||||
Pygments
|
||||
@@ -1,149 +0,0 @@
|
||||
# This file is part of DarkFi (https://dark.fi)
|
||||
#
|
||||
# Copyright (C) 2020-2025 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/>.
|
||||
|
||||
"""
|
||||
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 explorer daemon JSON-RPC configuration
|
||||
URL = "127.0.0.1"
|
||||
PORT = 14567
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
message = plaintext
|
||||
response = json.loads(message)
|
||||
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()
|
||||
|
||||
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.")
|
||||
abort(500)
|
||||
channel = Channel(reader, writer)
|
||||
return channel
|
||||
|
||||
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),
|
||||
"method": method,
|
||||
"params": params,
|
||||
"jsonrpc": "2.0",
|
||||
}
|
||||
|
||||
# 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")
|
||||
abort(500)
|
||||
|
||||
# Erroneous query is handled with not found
|
||||
if "error" in response:
|
||||
error = response["error"]
|
||||
errcode, errmsg = error["code"], error["message"]
|
||||
print(f"error: {errcode} - {errmsg}")
|
||||
abort(404)
|
||||
|
||||
return response["result"]
|
||||
|
||||
async def get_last_n_blocks(n: str):
|
||||
"""Retrieves the last n blocks."""
|
||||
return await query("blocks.get_last_n_blocks", [n])
|
||||
|
||||
async def get_basic_statistics():
|
||||
"""Retrieves basic statistics."""
|
||||
return await query("statistics.get_basic_statistics", [])
|
||||
|
||||
async def get_metric_statistics():
|
||||
"""Retrieves metrics statistics."""
|
||||
return await query("statistics.get_metric_statistics", [])
|
||||
|
||||
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])
|
||||
|
||||
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])
|
||||
|
||||
|
||||
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])
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,208 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 25.4.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 687.3 179" style="enable-background:new 0 0 687.3 179;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FFFFFF;}
|
||||
</style>
|
||||
<desc>Generated with Qt</desc>
|
||||
<g transform="matrix(1.45797,0,0,1.45797,502.142,172.042)">
|
||||
</g>
|
||||
<g transform="matrix(1.45797,0,0,1.45797,502.142,172.042)">
|
||||
</g>
|
||||
<g transform="matrix(1.45797,0,0,1.45797,502.142,172.042)">
|
||||
</g>
|
||||
<g transform="matrix(1.45797,0,0,1.45797,502.142,172.042)">
|
||||
</g>
|
||||
<g transform="matrix(1.45797,0,0,1.45797,502.142,172.042)">
|
||||
</g>
|
||||
<g transform="matrix(1.45797,0,0,1.45797,502.142,172.042)">
|
||||
</g>
|
||||
<g transform="matrix(1.45797,0,0,1.45797,502.142,172.042)">
|
||||
</g>
|
||||
<g transform="matrix(1.45797,0,0,1.45797,502.142,172.042)">
|
||||
</g>
|
||||
<g transform="matrix(1.45797,0,0,1.45797,502.142,172.042)">
|
||||
</g>
|
||||
<g transform="matrix(1.45797,0,0,1.45797,502.142,172.042)">
|
||||
</g>
|
||||
<g transform="matrix(1.45797,0,0,1.45797,502.142,172.042)">
|
||||
</g>
|
||||
<g transform="matrix(1.45797,0,0,1.45797,502.142,172.042)">
|
||||
</g>
|
||||
<g transform="matrix(1.45797,0,0,1.45797,502.142,172.042)">
|
||||
</g>
|
||||
<g transform="matrix(1.45797,0,0,1.45797,502.142,172.042)">
|
||||
</g>
|
||||
<g transform="matrix(1.45797,0,0,1.45797,502.142,172.042)">
|
||||
</g>
|
||||
<g transform="matrix(1.45797,0,0,1.45797,502.142,172.042)">
|
||||
</g>
|
||||
<g transform="matrix(0.18662,0,0,-0.18662,548.797,376.392)">
|
||||
</g>
|
||||
<g transform="matrix(0.18662,0,0,-0.18662,548.797,376.392)">
|
||||
</g>
|
||||
<g transform="matrix(0.18662,0,0,-0.18662,548.797,376.392)">
|
||||
</g>
|
||||
<g transform="matrix(0.18662,0,0,-0.18662,688.949,376.392)">
|
||||
</g>
|
||||
<g transform="matrix(0.18662,0,0,-0.18662,688.949,376.392)">
|
||||
</g>
|
||||
<g transform="matrix(0.18662,0,0,-0.18662,688.949,376.392)">
|
||||
</g>
|
||||
<g transform="matrix(0.18662,0,0,-0.18662,777.034,376.392)">
|
||||
</g>
|
||||
<g transform="matrix(0.18662,0,0,-0.18662,777.034,376.392)">
|
||||
</g>
|
||||
<g transform="matrix(0.18662,0,0,-0.18662,777.034,376.392)">
|
||||
</g>
|
||||
<g transform="matrix(0.18662,0,0,-0.18662,834.513,376.392)">
|
||||
</g>
|
||||
<g transform="matrix(0.18662,0,0,-0.18662,834.513,376.392)">
|
||||
</g>
|
||||
<g transform="matrix(0.18662,0,0,-0.18662,834.513,376.392)">
|
||||
</g>
|
||||
<g transform="matrix(0.18662,0,0,-0.18662,834.513,376.392)">
|
||||
</g>
|
||||
<g transform="matrix(0.18662,0,0,-0.18662,938.46,376.392)">
|
||||
</g>
|
||||
<g transform="matrix(0.18662,0,0,-0.18662,938.46,376.392)">
|
||||
</g>
|
||||
<g transform="matrix(0.18662,0,0,-0.18662,938.46,376.392)">
|
||||
</g>
|
||||
<g transform="matrix(0.18662,0,0,-0.18662,938.46,376.392)">
|
||||
</g>
|
||||
<g transform="matrix(0.18662,0,0,-0.18662,1054.91,376.392)">
|
||||
</g>
|
||||
<g transform="matrix(0.18662,0,0,-0.18662,1054.91,376.392)">
|
||||
</g>
|
||||
<g transform="matrix(0.18662,0,0,-0.18662,1054.91,376.392)">
|
||||
</g>
|
||||
<g transform="matrix(0.18662,0,0,-0.18662,1054.91,376.392)">
|
||||
</g>
|
||||
<g transform="matrix(0.18662,0,0,-0.18662,548.797,579.808)">
|
||||
</g>
|
||||
<g transform="matrix(0.18662,0,0,-0.18662,548.797,579.808)">
|
||||
</g>
|
||||
<g transform="matrix(0.18662,0,0,-0.18662,548.797,579.808)">
|
||||
</g>
|
||||
<g transform="matrix(0.18662,0,0,-0.18662,688.949,579.808)">
|
||||
</g>
|
||||
<g transform="matrix(0.18662,0,0,-0.18662,688.949,579.808)">
|
||||
</g>
|
||||
<g transform="matrix(0.18662,0,0,-0.18662,688.949,579.808)">
|
||||
</g>
|
||||
<g transform="matrix(0.18662,0,0,-0.18662,777.034,579.808)">
|
||||
</g>
|
||||
<g transform="matrix(0.18662,0,0,-0.18662,777.034,579.808)">
|
||||
</g>
|
||||
<g transform="matrix(0.18662,0,0,-0.18662,777.034,579.808)">
|
||||
</g>
|
||||
<g transform="matrix(0.18662,0,0,-0.18662,834.513,579.808)">
|
||||
</g>
|
||||
<g transform="matrix(0.18662,0,0,-0.18662,834.513,579.808)">
|
||||
</g>
|
||||
<g transform="matrix(0.18662,0,0,-0.18662,834.513,579.808)">
|
||||
</g>
|
||||
<g transform="matrix(0.18662,0,0,-0.18662,834.513,579.808)">
|
||||
</g>
|
||||
<g transform="matrix(0.18662,0,0,-0.18662,938.46,579.808)">
|
||||
</g>
|
||||
<g transform="matrix(0.18662,0,0,-0.18662,938.46,579.808)">
|
||||
</g>
|
||||
<g transform="matrix(0.18662,0,0,-0.18662,938.46,579.808)">
|
||||
</g>
|
||||
<g transform="matrix(0.18662,0,0,-0.18662,954.51,579.808)">
|
||||
</g>
|
||||
<g transform="matrix(0.18662,0,0,-0.18662,954.51,579.808)">
|
||||
</g>
|
||||
<g transform="matrix(0.18662,0,0,-0.18662,954.51,579.808)">
|
||||
</g>
|
||||
<g transform="matrix(0.18662,0,0,-0.18662,954.51,579.808)">
|
||||
</g>
|
||||
<g transform="matrix(0.18662,0,0,-0.18662,1070.96,579.808)">
|
||||
</g>
|
||||
<g transform="matrix(0.18662,0,0,-0.18662,1070.96,579.808)">
|
||||
</g>
|
||||
<g transform="matrix(0.18662,0,0,-0.18662,1070.96,579.808)">
|
||||
</g>
|
||||
<g transform="matrix(0.18662,0,0,-0.18662,1070.96,579.808)">
|
||||
</g>
|
||||
<g transform="matrix(1.45797,0,0,1.45797,502.142,172.042)">
|
||||
</g>
|
||||
<g transform="matrix(1.45797,0,0,1.45797,502.142,172.042)">
|
||||
</g>
|
||||
<g transform="matrix(1.45797,0,0,1.45797,502.142,172.042)">
|
||||
</g>
|
||||
<g transform="matrix(1.45797,0,0,1.45797,502.142,172.042)">
|
||||
</g>
|
||||
<g transform="matrix(1.45797,0,0,1.45797,502.142,172.042)">
|
||||
</g>
|
||||
<g transform="matrix(1.45797,0,0,1.45797,502.142,172.042)">
|
||||
</g>
|
||||
<g transform="matrix(1.45797,0,0,1.45797,502.142,172.042)">
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,0,0)">
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,0,0)">
|
||||
</g>
|
||||
<g transform="matrix(1.45797,0,0,1.45797,502.142,172.042)">
|
||||
</g>
|
||||
<g transform="matrix(1.45797,0,0,1.45797,502.142,172.042)">
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,0,0)">
|
||||
<path vector-effect="non-scaling-stroke" class="st0" d="M295.6,49H287c-2.3,2.1-4.5,4.4-5.8,7.3c-1.4,3-1.5,6.4-1.5,9.6v11.8v15.4
|
||||
c0,3,0,5.9-0.8,8.8c-0.8,2.9-2.1,5.6-3.8,8c-5.3,7.6-14.7,12.6-23.8,14.9l-1.4-3.6c1.8-1.1,3.5-2.6,4.9-4.2
|
||||
c2.4-3.2,3.6-7.1,4.1-11.1l0.3-33.1c0-1.7,0-3.3,0.3-4.9c0.3-1.7,0.9-3.2,1.8-4.7c1.8-2.7,4.5-4.9,7.3-6.5l12.9-7.7h-24.1
|
||||
c-1.4,0-2.9,0-4.1-0.6c-1.4-0.5-2.6-1.2-3.5-2.3c-1.2-1.5-2.1-3.3-2.6-5.2c-0.5-2-0.8-3.9-0.8-5.8c-0.2-3.9,0.5-7.9,1.4-11.7
|
||||
l3.3,0.2c0.3,1.2,0.8,2.3,1.5,3.3c0.6,0.9,1.7,1.7,2.7,2.3c0.9,0.5,2.1,0.9,3.3,1.1c1.2,0.2,2.4,0.2,3.5,0.2h38.5
|
||||
c6.2,0,12.7,0.3,18.5,2.7c5.9,2.4,10.9,6.7,14.7,11.8c6.4,8.9,8.9,20,9.7,30.9c0.5,10.5-0.8,21.1-3.6,31.2
|
||||
c-1.7,5.5-3.8,10.9-6.2,16.1l-37.9,20.9c-7.3-5.3-16.2-8.3-25.2-8.5c-11.5-0.3-19.1,2.9-27,11.8h-4.2c9.3-14.3,17-24.4,34.4-27.9
|
||||
c13.8-2.7,29.6,1.8,40.8,10.5c3-7.9,5.3-16.1,6.8-24.4c1.2-7.9,1.8-15.6,1.5-23.5c0-8.5-2.6-19-8.2-25.6c-2.4-2.7-5.6-5-9.1-6.2
|
||||
C302.5,49.3,299,49,295.6,49"/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,0,0)">
|
||||
<path vector-effect="non-scaling-stroke" class="st0" d="M400.3,123.1V80.6l-23.4-15l-16.7,8.8c-1.8,0.9-3.5,1.8-5,3.2
|
||||
c-1.4,1.2-2.6,2.7-3.3,4.4c-0.8,1.7-1.2,3.6-1.1,5.5c0.2,2,0.8,3.9,1.8,5.6c2.1,3.2,5.5,5.5,8.9,7.1c0.8,0.3,1.4,0.8,2.1,1.1
|
||||
c-3.5,1.8-6.8,4.1-9.9,6.7c-2.1,1.8-4.1,4.1-5.6,6.5s-2.4,5.2-2.6,8c-0.3,4.4,1.5,8.9,4.2,12.3c3.9,4.9,9.9,7.9,16.1,8.6l16.1-11.7
|
||||
l11.5,11.7l16.1-11.2L400.3,123.1 M381.3,102.4c-2.7-0.6-5.5-1.2-8.2-2.1c-1.8-0.6-3.6-1.2-5.5-1.8c-1.1-0.5-2-0.9-3-1.4
|
||||
c-2.7-1.4-5.3-3-7.3-5.3c-1.4-1.7-2.3-3.6-2.3-5.8c0-1.5,0.5-2.9,1.2-4.2c0.8-1.4,1.7-2.4,2.9-3.3c0.9-0.8,2.1-1.4,3.2-1.8l19,11.7
|
||||
C381.3,93,381.3,97.7,381.3,102.4 M381.3,106.8c0,7.7,0,15.5,0,23.2c-1.7,0.5-3.3,0.6-5,0.3c-2.1-0.5-4.2-1.5-5.9-3
|
||||
c-1.7-1.5-2.9-3.2-3.9-5.2c-2.1-4.1-3.2-8.8-2.4-13.3c0.3-2.3,1.2-4.5,2.4-6.5c2,0.6,3.9,1.4,5.9,2
|
||||
C375.4,105.1,378.3,105.8,381.3,106.8"/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,0,0)">
|
||||
<path vector-effect="non-scaling-stroke" class="st0" d="M453.5,123.7l-8.2,5.5l-5.9-4.7V79.8c2.1,2.1,4.7,3.8,7.4,5
|
||||
c1.8,0.8,3.6,1.4,5.6,1.4s3.8-0.5,5.6-1.2c2.3-1.1,4.1-2.7,5.5-4.9c1.4-2.1,2.3-4.4,2.9-6.8c0.8-2.7,1.4-5.5,1.7-8.3h-2.9
|
||||
c-1.4,2.3-3.6,4.2-7.9,4.2c-3.6,0-6.1-2-7.4-4.2h-1.7l-10.5,12l-11.2-12l-15.8,11.1l8.5,8.6v39.9L415,129l18.8,14.6l22.6-16.4
|
||||
L453.5,123.7"/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,0,0)">
|
||||
<path vector-effect="none" class="st0" d="M544,125.3l-6.5,4.4l-29.7-44.3l1.4-3c2.1,2,4.9,3.6,7.6,4.4c2.3,0.8,4.9,0.9,7.3,0.6
|
||||
c2.4-0.3,4.7-1.2,6.7-2.6c2.1-1.5,3.9-3.5,5.3-5.6c1.4-2.1,2.3-4.7,3-7.1c0.6-2.1,1.1-4.2,1.4-6.4h-2.9c-0.9,1.7-2.3,3.2-3.9,4.2
|
||||
c-1.7,1.1-3.5,1.5-5.5,1.5c-2.1,0-4.1-0.3-5.9-1.4s-3.2-2.6-4.2-4.4h-1.5l-22.4,34.9l29.7,43.7l23.4-15.3L544,125.3"/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,0,0)">
|
||||
<path vector-effect="non-scaling-stroke" class="st0" d="M492.5,124.5V26.4h-2.9l-24.7,17.4l7.1,7.9c0.2,0.8,0.2,1.5,0.2,2.3v69.1
|
||||
l-5,3.2l18.8,16.4l16.1-10.8L492.5,124.5"/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,0,0)">
|
||||
<path vector-effect="none" class="st0" d="M565,93.4h-26.4l13.2,23L565,93.4"/>
|
||||
</g>
|
||||
<path class="st0" d="M610.3,42c7.4,7.4,14.3,10,22.9,11.1l22-17.7l-3.6-3.2l-4.2,3.2c-12.6,0-25.5-5.9-29-11.5L589,49.8v20.6h-6.5
|
||||
l-14.1,12v3.3c21.4,0,3.3,0,20.6,0v30.2c0,9.9-0.2,14.7-1.5,17.7c-4.1-1.5-8.6-2.9-15.3-2c-17.1,2.3-29.1,16.7-33.5,25.8h3.8
|
||||
c0,0,4.9-8.9,19.3-8.9c5,0,9.3,1.7,12.3,3.9c0,0,28.1-25.3,36.4-35.6V85.7h12.4l19.7-12.9v-3h-32.1V42H610.3z"/>
|
||||
<g transform="matrix(1,0,0,1,0,0)">
|
||||
<path vector-effect="none" class="st0" d="M683.8,125.3l-6.7,4.4l-5-5.2V78.7l-11.7-13.8l-15.2,10.3l7.9,9.4v39.9l-4.9,3.5
|
||||
l17.3,15.5l21.8-15.3L683.8,125.3"/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,0,0)">
|
||||
<path vector-effect="none" class="st0" d="M661.6,34.5l11.3,12.3L660.7,58l-11.3-12.3L661.6,34.5"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="st0" d="M103.3,38.4c-12.4,0-22.4,10-22.4,22.4s10,22.4,22.4,22.4s22.4-10,22.4-22.4S115.7,38.4,103.3,38.4z M103.3,71
|
||||
c-5.6,0-10.1-4.5-10.1-10.1s4.5-10.1,10.1-10.1s10.1,4.5,10.1,10.1S108.9,71,103.3,71z"/>
|
||||
<path class="st0" d="M0,0l103.3,179L206.6,0H0L0,0z M103.3,92.8c-20.1,0-37.2-16-45.9-32c8.7-16,25.8-32,45.9-32s37.2,16,45.9,32
|
||||
C140.5,76.8,123.5,92.8,103.3,92.8z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 9.3 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 222 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 317 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.9 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 6.8 KiB |
File diff suppressed because it is too large
Load Diff
@@ -1,35 +0,0 @@
|
||||
<!--
|
||||
This file is part of DarkFi (https://dark.fi)
|
||||
|
||||
Copyright (C) 2020-2025 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 %}Not found!{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="background-container">
|
||||
<img src="{{ url_for('static', filename='img/darkfi25.jpg') }}" style="width:100%;">
|
||||
<div class="top-centered">
|
||||
<h1 class="display-4 text-white border-l3" style="font-family:'Weissrundgotisch';line-height:.9em;">
|
||||
Is this what you are looking for?
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
<!--
|
||||
This file is part of DarkFi (https://dark.fi)
|
||||
|
||||
Copyright (C) 2020-2025 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 %}Not found!{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="background-container">
|
||||
<img src="{{ url_for('static', filename='img/darkfi05.jpg') }}" style="width:100%">
|
||||
<div class="top-centered">
|
||||
<h1 class="display-4 text-white border-l3" style="font-family:'Weissrundgotisch';line-height:.9em">
|
||||
Reality fractured beyond repair!
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,189 +0,0 @@
|
||||
<!--
|
||||
This file is part of DarkFi (https://dark.fi)
|
||||
|
||||
Copyright (C) 2020-2025 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/>.
|
||||
-->
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<title>DarkFi – {% block title %}{% endblock %}</title>
|
||||
|
||||
<link rel="icon" type="image/png" href="{{ url_for('static', filename= 'img/icon.png') }}"/>
|
||||
|
||||
<!-- Latest compiled and minified CSS -->
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename= 'bootstrap.min.css') }}">
|
||||
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename= 'style.css') }}"/>
|
||||
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div class="page-container">
|
||||
<nav class="navbar navbar-expand-md navbar-dark tedt border-bottom border-secondary">
|
||||
<a class="navbar-brand" href="https://dark.fi/">
|
||||
<img src="{{ url_for('static', filename= 'img/darkfi.svg') }}" style="height:40px;"/>
|
||||
</a>
|
||||
|
||||
<div class="collapse navbar-collapse nav justify-content-end" id="collapsibleNavbar" style="height:40px;">
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-light" href="/">Explorer</a>
|
||||
</li>
|
||||
|
||||
<!--Twitter-->
|
||||
<li class="nav-item" style="padding-left:10px;">
|
||||
<div class="icon-sm">
|
||||
<a href="https://twitter.com/DarkFiSquad" class="">
|
||||
<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 100 100"
|
||||
style="enable-background:new 0 0 100 100; max-width:30px; min-width:15px;"
|
||||
xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0 {
|
||||
fill: #3a3a3a;
|
||||
}
|
||||
</style>
|
||||
<g id="layer1" transform="translate(-539.17946,-568.85777)">
|
||||
<path id="path3611" class="st0"
|
||||
d="M570.9,659c37.3,0,57.7-30.9,57.7-57.7c0-0.9,0-1.8-0.1-2.6c4-2.9,7.4-6.4,10.1-10.5
|
||||
c-3.6,1.6-7.5,2.7-11.6,3.2c4.2-2.5,7.4-6.5,8.9-11.2c-3.9,2.3-8.3,4-12.9,4.9c-3.7-3.9
|
||||
-9-6.4-14.8-6.4c-11.2,0-20.3,9.1-20.3,20.3c0,1.6,0.2,3.1,0.5,4.6c-16.8-0.8-31.8-8.9
|
||||
-41.8-21.2c-1.7,3-2.7,6.5-2.7,10.2c0,7,3.6,13.2,9,16.9c-3.3-0.1-6.5-1-9.2-2.5c0,0.1,0,0.2,0,
|
||||
0.3c0,9.8,7,18,16.3,19.9c-1.7,0.5-3.5,0.7-5.3,0.7c-1.3,0-2.6-0.1-3.8-0.4c2.6,8.1,10.1,13.9,
|
||||
18.9,14.1c-6.9,5.4-15.7,8.7-25.2,8.7c-1.6,0-3.2-0.1-4.8-0.3C548.7,655.7,559.4,659,570.9,659"/>
|
||||
</g>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<!--Codeberg-->
|
||||
<li class="nav-item" style="padding-left:10px;">
|
||||
<div class="icon-sm">
|
||||
<a href="https://codeberg.org/darkrenaissance/darkfi" class="">
|
||||
<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 100 100" style="enable-background:new 0 0 100 100;"
|
||||
xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0 {
|
||||
fill: #3A3A3A;
|
||||
}
|
||||
</style>
|
||||
<g id="g370484" transform="matrix(0.06551432,0,0,0.06551432,-2.232417,-1.431776)">
|
||||
<g>
|
||||
<path id="path6733-5_00000147220371580420933290000015044290908562073483_"
|
||||
class="st0"
|
||||
d="M797.5,506.6c-2.6,0-4.7,1.6-4.7,3.6c0,0.2,0,0.4,0.1,0.6l253.7,1037.5c104.2-35.5,
|
||||
265.5-150.8,346-256.8l-591-783.2C800.8,507.2,799.2,506.6,797.5,506.6z"/>
|
||||
<path id="path360787_00000175324937253379690650000018049397027717501353_"
|
||||
class="st0"
|
||||
d="M797.3,59C375.7,59,34.1,400.6,34.1,822.1c0,143.4,40.4,283.8,
|
||||
116.5,405.3l636.3-822.6c4.6-5.9,16.1-5.9,20.6,0l636.3,822.6c76.2-121.5,
|
||||
116.6-262,116.6-405.4C1560.5,400.6,1218.8,59,797.3,59L797.3,59z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<!--Darkirc-->
|
||||
<li class="nav-item" style="padding-left:10px;">
|
||||
<div class="icon-sm">
|
||||
<a href="https://darkrenaissance.github.io/darkfi/misc/darkirc/darkirc.html" class="">
|
||||
<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 100 100"
|
||||
style="enable-background:new 0 0 100 100; max-width:30px; min-width:15px;"
|
||||
xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0 {
|
||||
fill: #3a3a3a;
|
||||
}
|
||||
</style>
|
||||
<path class="st0" d="M0,0v100h100V0H0z M16.2,30c0-2,1.2-3.6,4.5-3.6c3.4,0,4.5,1.5,4.5,3.6v1.5c0,2-1.2,3.6-4.5,3.6
|
||||
c-3.4,0-4.5-1.6-4.5-3.6V30z M32.3,64.8H8.6v-5.9H17V44.3H8.6v-5.9h15.9v20.5h7.8V64.8z M63.1,45.4h-5.6c-3.7,0-5.7,2.5-5.7,5.7v7.8
|
||||
h8.7v5.9H38.2v-5.9h6.1V44.3h-6.1v-5.9h13.6v7.4h0.4c1-3.9,3.2-7.4,8.4-7.4h2.5V45.4z M81.4,65.4c-8.3,0-13.3-5.3-13.3-13.8
|
||||
c0-8.5,5-13.8,13.3-13.8c5.8,0,9.1,2.6,11,6.4l-5.8,3.2c-0.9-2-2.3-3.6-5.2-3.6C78,43.8,76,46,76,49.5v4.3c0,3.5,1.9,5.6,5.5,5.6
|
||||
c2.9,0,4.4-1.5,5.6-3.7l5.7,3.3C90.9,62.8,87.3,65.4,81.4,65.4z"/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<!--Telegram-->
|
||||
<li class="nav-item" style="padding-left:10px; padding-right:10px;">
|
||||
<div class="icon-sm">
|
||||
<a href="https://t.me/darkfichat" class="">
|
||||
<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 100 100"
|
||||
style="enable-background:new 0 0 100 100; max-width:30px; min-width:15px;"
|
||||
xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0 {
|
||||
fill: #3a3a3a;
|
||||
}
|
||||
</style>
|
||||
<path class="st0" d="M50,0C22.4,0,0,22.4,0,50s22.4,50,50,50s50-22.4,50-50S77.6,0,50,0z M73.8,32.5c-0.1,2-1.4,9.1-2.5,16.8
|
||||
L67.9,72c0,0-0.3,3.3-2.9,3.9c-2.6,0.6-6.5-2-7.2-2.6c-0.6-0.4-10.8-6.9-14.5-10.1c-1-0.9-2.2-2.6,0.1-4.6l15.2-14.5
|
||||
c1.7-1.7,3.5-5.8-3.8-0.9L34.7,56.9c0,0-2.3,1.4-6.6,0.1l-9.4-2.9c0,0-3.5-2.2,2.5-4.3c14.5-6.8,32.2-13.7,48-20.2
|
||||
C69.1,29.6,74.3,27.6,73.8,32.5z"/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="navbarb">
|
||||
<div class="navbar2">
|
||||
<div class="container2 nav-container2">
|
||||
<input class="checkbox" type="checkbox" name="" id=""/>
|
||||
<div class="hamburger-lines">
|
||||
<span class="line line1"></span>
|
||||
<span class="line line2"></span>
|
||||
<span class="line line3"></span>
|
||||
</div>
|
||||
<div class="menu-items">
|
||||
<li class="topnav"><a href="/">Explorer</a></li>
|
||||
<li><a href="https://twitter.com/DarkFiSquad">Twitter</a></li>
|
||||
<li><a href="https://codeberg.org/darkrenaissance/darkfi">Codeberg</a></li>
|
||||
<li><a href="https://darkrenaissance.github.io/darkfi/misc/darkirc/darkirc.html">Darkirc</a>
|
||||
</li>
|
||||
<li><a href="https://t.me/darkfichat">Telegram</a></li>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div>
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
|
||||
<footer></footer>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
<!--
|
||||
This file is part of DarkFi (https://dark.fi)
|
||||
|
||||
Copyright (C) 2020-2025 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 %}Block {{ block[0] }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="content-container">
|
||||
|
||||
<!-- Block Details Header -->
|
||||
<div class="content-header" style="padding-bottom: 8px">
|
||||
<h2 class="content-section-header">Block Details</h2>
|
||||
<div class="">{{ block[0] }}</div>
|
||||
</div>
|
||||
|
||||
<!-- Block Details -->
|
||||
<ul>
|
||||
<li><strong>Version:</strong> {{ block[1] }}</li>
|
||||
<li><strong>Previous:</strong> <a href="../block/{{ block[2] }}">{{ block[2] }}</a></li>
|
||||
<li><strong>Height:</strong> {{ block[3] }}</li>
|
||||
<li><strong>Timestamp:</strong> {{ block[4] }}</li>
|
||||
<li><strong>Nonce:</strong> {{ block[5] }}</li>
|
||||
<li><strong>Root:</strong> {{ block[6] }}</li>
|
||||
<li><strong>Signature:</strong> {{ block[7] }}</li>
|
||||
</ul>
|
||||
|
||||
<!-- Page Content Divider -->
|
||||
<hr class="content-divider"/>
|
||||
|
||||
<!-- Block Transactions -->
|
||||
<div class="content-section" style="padding-top: 12px">
|
||||
<h2 class="content-section-header">Block Transactions</h2>
|
||||
<ul>
|
||||
{% for transaction in transactions %}
|
||||
<li><a href="../tx/{{ transaction[0] }}">{{ transaction[0] }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
<!--
|
||||
This file is part of DarkFi (https://dark.fi)
|
||||
|
||||
Copyright (C) 2020-2025 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 %}
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
<!--
|
||||
This file is part of DarkFi (https://dark.fi)
|
||||
|
||||
Copyright (C) 2020-2025 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 %}
|
||||
@@ -1,147 +0,0 @@
|
||||
<!--
|
||||
This file is part of DarkFi (https://dark.fi)
|
||||
|
||||
Copyright (C) 2020-2025 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 %}Explorer{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="content-container">
|
||||
|
||||
<!-- Explorer Search -->
|
||||
<div class="search-container">
|
||||
<h1>Explore</h1>
|
||||
<form action="/search">
|
||||
<input class="search-input" type="text" placeholder="Search for a block or transaction.." name="search_hash">
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!--Network Summary -->
|
||||
<div class="content-section" style="padding-bottom: 0px;">
|
||||
<h2 class="content-section-header">Network Summary</h2>
|
||||
<ul>
|
||||
<li>Height: {{ basic_stats[0] }}</li>
|
||||
<li>Epoch: {{ basic_stats[1] }}</li>
|
||||
<li>Last block: <a href="block/{{ basic_stats[2] }}">{{ basic_stats[2] }}</a></li>
|
||||
<li>Total blocks: {{ basic_stats[3] }}</li>
|
||||
<li>Total transactions: {{ basic_stats[4] }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!--Latest Blocks -->
|
||||
<div class="content-section">
|
||||
<h2 class="content-section-header">Latest Blocks</h2>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Height</th>
|
||||
<th>Hash</th>
|
||||
<th>Timestamp</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for block in blocks %}
|
||||
<tr>
|
||||
<td>{{ block[3] }}</td>
|
||||
<td><a href="block/{{ block[0] }}">{{ block[0] }}</a></td>
|
||||
<td>{{ block[4] }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!--Total Network Gas Consumption -->
|
||||
<div class="content-section">
|
||||
<h2 class="content-section-header">Total Network Gas Consumption</h2>
|
||||
{% if metric_stats %}
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Average</th>
|
||||
<th>Minimum</th>
|
||||
<th>Maximum</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Total</td>
|
||||
<td>{{ metric_stats[0] }}</td>
|
||||
<td>{{ metric_stats[1] }}</td>
|
||||
<td>{{ metric_stats[2] }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>WASM</td>
|
||||
<td>{{ metric_stats[3] }}</td>
|
||||
<td>{{ metric_stats[4] }}</td>
|
||||
<td>{{ metric_stats[5] }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>ZK Circuits</td>
|
||||
<td>{{ metric_stats[6] }}</td>
|
||||
<td>{{ metric_stats[7] }}</td>
|
||||
<td>{{ metric_stats[8] }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Signatures</td>
|
||||
<td>{{ metric_stats[9] }}</td>
|
||||
<td>{{ metric_stats[10] }}</td>
|
||||
<td>{{ metric_stats[11] }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Deployments</td>
|
||||
<td>{{ metric_stats[12] }}</td>
|
||||
<td>{{ metric_stats[13] }}</td>
|
||||
<td>{{ metric_stats[14] }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
Gas consumption details are not available.
|
||||
{% 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 %}
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
<!--
|
||||
This file is part of DarkFi (https://dark.fi)
|
||||
|
||||
Copyright (C) 2020-2025 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 %}Transaction {{ transaction[0] }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="content-container">
|
||||
|
||||
<!-- Page Content Header -->
|
||||
<div class="content-header" style="padding-bottom: 8px">
|
||||
<h1 class="content-section-header">Transaction Details</h1>
|
||||
<div class="">{{ transaction[0] }}</div>
|
||||
</div>
|
||||
|
||||
<!-- Transaction Details -->
|
||||
<ul>
|
||||
<li><strong>Timestamp:</strong> {{ transaction[3] }}</li>
|
||||
<li><strong>Block:</strong> <a href="../block/{{ transaction[1] }}">{{ transaction[1] }}</a></li>
|
||||
<li><strong>Payload:</strong> {{ transaction[2] }}</li>
|
||||
</ul>
|
||||
|
||||
<!-- Page Content Divider -->
|
||||
<hr class="content-divider"/>
|
||||
|
||||
<!-- Transaction Gas Consumption -->
|
||||
<div class="content-section" style="padding-top: 12px;">
|
||||
<h2 class="content-section-header">Gas Consumption</h2>
|
||||
<ul>
|
||||
<li><strong>Total:</strong> {{ transaction[4] }}</li>
|
||||
<li><strong>WASM:</strong> {{ transaction[5] }}</li>
|
||||
<li><strong>ZK Circuits:</strong> {{ transaction[6] }}</li>
|
||||
<li><strong>Signatures:</strong> {{ transaction[7] }}</li>
|
||||
<li><strong>Deployments:</strong> {{ transaction[8] }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,324 +0,0 @@
|
||||
/* This file is part of DarkFi (https://dark.fi)
|
||||
*
|
||||
* Copyright (C) 2020-2025 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/>.
|
||||
*/
|
||||
|
||||
use log::{debug, warn};
|
||||
use sled_overlay::sled::{transaction::ConflictableTransactionError, Transactional};
|
||||
use tinyjson::JsonValue;
|
||||
|
||||
use darkfi::{
|
||||
blockchain::{
|
||||
BlockInfo, BlockchainOverlay, HeaderHash, SLED_BLOCK_DIFFICULTY_TREE,
|
||||
SLED_BLOCK_ORDER_TREE, SLED_BLOCK_TREE,
|
||||
},
|
||||
util::time::Timestamp,
|
||||
Error, Result,
|
||||
};
|
||||
use darkfi_sdk::{crypto::schnorr::Signature, tx::TransactionHash};
|
||||
|
||||
use crate::ExplorerService;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
/// Structure representing a block record.
|
||||
pub struct BlockRecord {
|
||||
/// Header hash identifier of the block
|
||||
pub header_hash: String,
|
||||
/// Block version
|
||||
pub version: u8,
|
||||
/// Previous block hash
|
||||
pub previous: String,
|
||||
/// Block height
|
||||
pub height: u32,
|
||||
/// Block creation timestamp
|
||||
pub timestamp: Timestamp,
|
||||
/// The block's nonce. This value changes arbitrarily with mining.
|
||||
pub nonce: u64,
|
||||
/// Merkle tree root of the transactions hashes contained in this block
|
||||
pub root: String,
|
||||
/// Block producer signature
|
||||
pub signature: Signature,
|
||||
}
|
||||
|
||||
impl BlockRecord {
|
||||
/// Auxiliary function to convert a `BlockRecord` into a `JsonValue` array.
|
||||
pub fn to_json_array(&self) -> JsonValue {
|
||||
JsonValue::Array(vec![
|
||||
JsonValue::String(self.header_hash.clone()),
|
||||
JsonValue::Number(self.version as f64),
|
||||
JsonValue::String(self.previous.clone()),
|
||||
JsonValue::Number(self.height as f64),
|
||||
JsonValue::String(self.timestamp.to_string()),
|
||||
JsonValue::Number(self.nonce as f64),
|
||||
JsonValue::String(self.root.clone()),
|
||||
JsonValue::String(format!("{:?}", self.signature)),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&BlockInfo> for BlockRecord {
|
||||
fn from(block: &BlockInfo) -> Self {
|
||||
Self {
|
||||
header_hash: block.hash().to_string(),
|
||||
version: block.header.version,
|
||||
previous: block.header.previous.to_string(),
|
||||
height: block.header.height,
|
||||
timestamp: block.header.timestamp,
|
||||
nonce: block.header.nonce,
|
||||
root: block.header.root.to_string(),
|
||||
signature: block.signature,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ExplorerService {
|
||||
/// Resets blocks in the database by clearing all block related trees, returning an Ok result on success.
|
||||
pub fn reset_blocks(&self) -> Result<()> {
|
||||
let db = &self.db.blockchain.sled_db;
|
||||
// Initialize block related trees to reset
|
||||
let trees_to_reset = [SLED_BLOCK_TREE, SLED_BLOCK_ORDER_TREE, SLED_BLOCK_DIFFICULTY_TREE];
|
||||
|
||||
// Iterate over each tree and remove its entries
|
||||
for tree_name in &trees_to_reset {
|
||||
let tree = db.open_tree(tree_name)?;
|
||||
tree.clear()?;
|
||||
let tree_name_str = std::str::from_utf8(tree_name)?;
|
||||
debug!(target: "blockchain-explorer::blocks", "Successfully reset block tree: {tree_name_str}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Adds the provided [`BlockInfo`] to the block explorer database.
|
||||
///
|
||||
/// This function processes each transaction in the block, calculating and updating the
|
||||
/// latest [`GasMetrics`] for non-genesis blocks and for transactions that are not
|
||||
/// PoW rewards. After processing all transactions, the block is permanently persisted to
|
||||
/// the explorer database.
|
||||
pub async fn put_block(&self, block: &BlockInfo) -> Result<()> {
|
||||
let blockchain_overlay = BlockchainOverlay::new(&self.db.blockchain)?;
|
||||
|
||||
// Initialize collections to hold gas data and transactions that have gas data
|
||||
let mut tx_gas_data = Vec::with_capacity(block.txs.len());
|
||||
let mut txs_hashes_with_gas_data = Vec::with_capacity(block.txs.len());
|
||||
|
||||
// Calculate gas data for non-PoW reward transactions and non-genesis blocks
|
||||
for (i, tx) in block.txs.iter().enumerate() {
|
||||
if !tx.is_pow_reward() && block.header.height != 0 {
|
||||
tx_gas_data.insert(i, self.calculate_tx_gas_data(tx, false).await?);
|
||||
txs_hashes_with_gas_data.insert(i, tx.hash());
|
||||
}
|
||||
}
|
||||
|
||||
// If the block contains transaction gas data, insert the gas metrics into the metrics store
|
||||
if !tx_gas_data.is_empty() {
|
||||
self.db.metrics_store.insert_gas_metrics(
|
||||
block.header.height,
|
||||
&block.header.timestamp,
|
||||
&txs_hashes_with_gas_data,
|
||||
&tx_gas_data,
|
||||
)?;
|
||||
}
|
||||
|
||||
// Add the block and commit the changes to persist it
|
||||
let _ = blockchain_overlay.lock().unwrap().add_block(block)?;
|
||||
blockchain_overlay.lock().unwrap().overlay.lock().unwrap().apply()?;
|
||||
debug!(target: "blockchain_explorer::blocks::put_block", "Added block {:?}", block);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Provides the total block count.
|
||||
pub fn get_block_count(&self) -> usize {
|
||||
self.db.blockchain.len()
|
||||
}
|
||||
|
||||
/// Fetch all known blocks from the database.
|
||||
pub fn get_blocks(&self) -> Result<Vec<BlockRecord>> {
|
||||
// Fetch blocks and handle any errors encountered
|
||||
let blocks = &self.db.blockchain.get_all().map_err(|e| {
|
||||
Error::DatabaseError(format!("[get_blocks] Block retrieval failed: {e:?}"))
|
||||
})?;
|
||||
|
||||
// Transform the found blocks into a vector of block records
|
||||
let block_records: Vec<BlockRecord> = blocks.iter().map(BlockRecord::from).collect();
|
||||
|
||||
Ok(block_records)
|
||||
}
|
||||
|
||||
/// Fetch a block given its header hash from the database.
|
||||
pub fn get_block_by_hash(&self, header_hash: &str) -> Result<Option<BlockRecord>> {
|
||||
// Parse header hash, returning an error if parsing fails
|
||||
let header_hash = header_hash
|
||||
.parse::<HeaderHash>()
|
||||
.map_err(|_| Error::ParseFailed("[get_block_by_hash] Invalid header hash"))?;
|
||||
|
||||
// Fetch block by hash and handle encountered errors
|
||||
match self.db.blockchain.get_blocks_by_hash(&[header_hash]) {
|
||||
Ok(blocks) => Ok(blocks.first().map(BlockRecord::from)),
|
||||
Err(Error::BlockNotFound(_)) => Ok(None),
|
||||
Err(e) => Err(Error::DatabaseError(format!(
|
||||
"[get_block_by_hash] Block retrieval failed: {e:?}"
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch a block given its height from the database.
|
||||
pub fn get_block_by_height(&self, height: u32) -> Result<Option<BlockRecord>> {
|
||||
// Fetch block by height and handle encountered errors
|
||||
match self.db.blockchain.get_blocks_by_heights(&[height]) {
|
||||
Ok(blocks) => Ok(blocks.first().map(BlockRecord::from)),
|
||||
Err(Error::BlockNotFound(_)) => Ok(None),
|
||||
Err(e) => Err(Error::DatabaseError(format!(
|
||||
"[get_block_by_height] Block retrieval failed: {e:?}"
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch the last block from the database.
|
||||
pub fn last_block(&self) -> Result<Option<(u32, String)>> {
|
||||
let block_store = &self.db.blockchain.blocks;
|
||||
|
||||
// Return None result when no blocks exist
|
||||
if block_store.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// Blocks exist, retrieve last block
|
||||
let (height, header_hash) = block_store.get_last().map_err(|e| {
|
||||
Error::DatabaseError(format!("[last_block] Block retrieval failed: {e:?}"))
|
||||
})?;
|
||||
|
||||
// Convert header hash to a string and return result
|
||||
Ok(Some((height, header_hash.to_string())))
|
||||
}
|
||||
|
||||
/// Fetch the last N blocks from the database.
|
||||
pub fn get_last_n(&self, n: usize) -> Result<Vec<BlockRecord>> {
|
||||
// Fetch the last n blocks and handle any errors encountered
|
||||
let blocks_result = &self.db.blockchain.get_last_n(n).map_err(|e| {
|
||||
Error::DatabaseError(format!("[get_last_n] Block retrieval failed: {e:?}"))
|
||||
})?;
|
||||
|
||||
// Transform the found blocks into a vector of block records
|
||||
let block_records: Vec<BlockRecord> = blocks_result.iter().map(BlockRecord::from).collect();
|
||||
|
||||
Ok(block_records)
|
||||
}
|
||||
|
||||
/// Fetch blocks within a specified range from the database.
|
||||
pub fn get_by_range(&self, start: u32, end: u32) -> Result<Vec<BlockRecord>> {
|
||||
// Fetch blocks in the specified range and handle any errors encountered
|
||||
let blocks_result = &self.db.blockchain.get_by_range(start, end).map_err(|e| {
|
||||
Error::DatabaseError(format!("[get_by_range]: Block retrieval failed: {e:?}"))
|
||||
})?;
|
||||
|
||||
// Transform the found blocks into a vector of block records
|
||||
let block_records: Vec<BlockRecord> = blocks_result.iter().map(BlockRecord::from).collect();
|
||||
|
||||
Ok(block_records)
|
||||
}
|
||||
|
||||
/// Resets the [`ExplorerDb::blockchain::blocks`] and [`ExplorerDb::blockchain::transactions`]
|
||||
/// trees to a specified height by removing entries above the `reset_height`, returning a result
|
||||
/// that indicates success or failure.
|
||||
///
|
||||
/// The function retrieves the last explorer block and iteratively rolls back entries
|
||||
/// in the [`BlockStore::main`], [`BlockStore::order`], and [`BlockStore::difficulty`] trees
|
||||
/// to the specified `reset_height`. It also resets the [`TxStore::main`] and
|
||||
/// [`TxStore::location`] trees to reflect the transaction state at the given height.
|
||||
///
|
||||
/// This operation is performed atomically using a sled transaction applied across the affected sled
|
||||
/// trees, ensuring consistency and avoiding partial updates.
|
||||
pub fn reset_to_height(&self, reset_height: u32) -> Result<()> {
|
||||
let block_store = &self.db.blockchain.blocks;
|
||||
let tx_store = &self.db.blockchain.transactions;
|
||||
|
||||
debug!(target: "blockchain_explorer::blocks::reset_to_height", "Resetting to height {reset_height}: block_count={}, txs_count={}", block_store.len(), tx_store.len());
|
||||
|
||||
// Get the last block height
|
||||
let (last_block_height, _) = block_store.get_last().map_err(|e| {
|
||||
Error::DatabaseError(format!(
|
||||
"[reset_to_height]: Failed to get the last block height: {e:?}"
|
||||
))
|
||||
})?;
|
||||
|
||||
// Skip resetting blocks if `reset_height` is greater than or equal to `last_block_height`
|
||||
if reset_height >= last_block_height {
|
||||
warn!(target: "blockchain_explorer::blocks::reset_to_height",
|
||||
"Nothing to reset because reset_height is greater than or equal to last_block_height: {reset_height} >= {last_block_height}");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Get the associated block infos in order to obtain transactions to reset
|
||||
let block_infos_to_reset =
|
||||
&self.db.blockchain.get_by_range(reset_height, last_block_height).map_err(|e| {
|
||||
Error::DatabaseError(format!(
|
||||
"[reset_to_height]: Failed to get the transaction hashes to reset: {e:?}"
|
||||
))
|
||||
})?;
|
||||
|
||||
// Collect the transaction hashes from the blocks that need resetting
|
||||
let txs_hashes_to_reset: Vec<TransactionHash> = block_infos_to_reset
|
||||
.iter()
|
||||
.flat_map(|block_info| block_info.txs.iter().map(|tx| tx.hash()))
|
||||
.collect();
|
||||
|
||||
// Perform the reset operation atomically using a sled transaction
|
||||
let tx_result = (&block_store.main, &block_store.order, &block_store.difficulty, &tx_store.main, &tx_store.location)
|
||||
.transaction(|(block_main, block_order, block_difficulty, tx_main, tx_location)| {
|
||||
// Traverse the block heights in reverse, removing each block up to (but not including) reset_height
|
||||
for height in (reset_height + 1..=last_block_height).rev() {
|
||||
let height_key = height.to_be_bytes();
|
||||
|
||||
// Fetch block from `order` tree to obtain the block hash needed to remove blocks from `main` tree
|
||||
let order_header_hash = block_order.get(height_key).map_err(ConflictableTransactionError::Abort)?;
|
||||
|
||||
if let Some(header_hash) = order_header_hash {
|
||||
|
||||
// Remove block from the `main` tree
|
||||
block_main.remove(&header_hash).map_err(ConflictableTransactionError::Abort)?;
|
||||
|
||||
// Remove block from the `difficulty` tree
|
||||
block_difficulty.remove(&height_key).map_err(ConflictableTransactionError::Abort)?;
|
||||
|
||||
// Remove block from the `order` tree
|
||||
block_order.remove(&height_key).map_err(ConflictableTransactionError::Abort)?;
|
||||
}
|
||||
|
||||
debug!(target: "blockchain_explorer::blocks::reset_to_height", "Removed block at height: {height}");
|
||||
}
|
||||
|
||||
// Iterate through the transaction hashes, removing the related transactions
|
||||
for (tx_count, tx_hash) in txs_hashes_to_reset.iter().enumerate() {
|
||||
// Remove transaction from the `main` tree
|
||||
tx_main.remove(tx_hash.inner()).map_err(ConflictableTransactionError::Abort)?;
|
||||
// Remove transaction from the `location` tree
|
||||
tx_location.remove(tx_hash.inner()).map_err(ConflictableTransactionError::Abort)?;
|
||||
debug!(target: "blockchain_explorer::blocks::reset_to_height", "Removed transaction ({tx_count}): {tx_hash}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.map_err(|e| {
|
||||
Error::DatabaseError(format!("[reset_to_height]: Resetting height failed: {e:?}"))
|
||||
});
|
||||
|
||||
debug!(target: "blockchain_explorer::blocks::reset_to_height", "Successfully reset to height {reset_height}: block_count={}, txs_count={}", block_store.len(), tx_store.len());
|
||||
|
||||
tx_result
|
||||
}
|
||||
}
|
||||
@@ -1,442 +0,0 @@
|
||||
/* This file is part of DarkFi (https://dark.fi)
|
||||
*
|
||||
* Copyright (C) 2020-2025 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/>.
|
||||
*/
|
||||
|
||||
use std::sync::{Arc, Mutex, MutexGuard};
|
||||
|
||||
use log::{debug, info};
|
||||
use sled_overlay::{sled, SledDbOverlay};
|
||||
|
||||
use darkfi::{blockchain::SledDbOverlayPtr, Error, Result};
|
||||
use darkfi_sdk::crypto::ContractId;
|
||||
use darkfi_serial::{async_trait, deserialize, serialize, SerialDecodable, SerialEncodable};
|
||||
|
||||
/// Contract metadata tree name.
|
||||
pub const SLED_CONTRACT_METADATA_TREE: &[u8] = b"_contact_metadata";
|
||||
|
||||
/// Contract source code tree name.
|
||||
pub const SLED_CONTRACT_SOURCE_CODE_TREE: &[u8] = b"_contact_source_code";
|
||||
|
||||
/// Represents contract metadata containing additional contract information that is not stored on-chain.
|
||||
#[derive(Debug, Clone, Eq, PartialEq, SerialEncodable, SerialDecodable)]
|
||||
pub struct ContractMetaData {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
impl ContractMetaData {
|
||||
pub fn new(name: String, description: String) -> Self {
|
||||
Self { name, description }
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a source file containing its file path as a string and its content as a vector of bytes.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ContractSourceFile {
|
||||
pub path: String,
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
impl ContractSourceFile {
|
||||
/// Creates a `ContractSourceFile` instance.
|
||||
pub fn new(path: String, content: String) -> Self {
|
||||
Self { path, content }
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ContractMetaStore {
|
||||
/// Pointer to the underlying sled database used by the store and its associated overlay.
|
||||
pub sled_db: sled::Db,
|
||||
|
||||
/// Primary sled tree for storing contract metadata, utilizing [`ContractId::to_string`] as keys
|
||||
/// and serialized [`ContractMetaData`] as values.
|
||||
pub main: sled::Tree,
|
||||
|
||||
/// Sled tree for storing contract source code, utilizing source file paths as keys pre-appended with a contract id
|
||||
/// and serialized contract source code [`ContractSourceFile`] content as values.
|
||||
pub source_code: sled::Tree,
|
||||
}
|
||||
|
||||
impl ContractMetaStore {
|
||||
/// Creates a `ContractMetaStore` instance.
|
||||
pub fn new(db: &sled::Db) -> Result<Self> {
|
||||
let main = db.open_tree(SLED_CONTRACT_METADATA_TREE)?;
|
||||
let source_code = db.open_tree(SLED_CONTRACT_SOURCE_CODE_TREE)?;
|
||||
|
||||
Ok(Self { sled_db: db.clone(), main, source_code })
|
||||
}
|
||||
|
||||
/// Retrieves associated contract metadata for a given [`ContractId`],
|
||||
/// returning an `Option` of [`ContractMetaData`] upon success.
|
||||
pub fn get(&self, contract_id: &ContractId) -> Result<Option<ContractMetaData>> {
|
||||
let opt = self.main.get(contract_id.to_string().as_bytes())?;
|
||||
opt.map(|bytes| deserialize(&bytes).map_err(Error::from)).transpose()
|
||||
}
|
||||
|
||||
/// Provides the number of stored [`ContractMetaData`].
|
||||
pub fn len(&self) -> usize {
|
||||
self.main.len()
|
||||
}
|
||||
|
||||
/// Checks if there is contract metadata stored.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.main.is_empty()
|
||||
}
|
||||
|
||||
/// Retrieves all the source file paths associated for provided [`ContractId`].
|
||||
///
|
||||
/// This function uses provided [`ContractId`] as a prefix to filter relevant paths
|
||||
/// stored in the underlying sled tree, ensuring only files belonging to
|
||||
/// the given contract ID are included. Returns a `Vec` of [`String`]
|
||||
/// representing source code paths.
|
||||
pub fn get_source_paths(&self, contract_id: &ContractId) -> Result<Vec<String>> {
|
||||
let prefix = format!("{}/", contract_id);
|
||||
|
||||
// Get all the source paths for provided `ContractId`
|
||||
let mut entries = self
|
||||
.source_code
|
||||
.scan_prefix(&prefix)
|
||||
.filter_map(|item| {
|
||||
let (key, _) = item.ok()?;
|
||||
let key_str = String::from_utf8(key.to_vec()).ok()?;
|
||||
key_str.strip_prefix(&prefix).map(|path| path.to_string())
|
||||
})
|
||||
.collect::<Vec<String>>();
|
||||
|
||||
// Sort the entries to ensure a consistent order
|
||||
entries.sort();
|
||||
|
||||
Ok(entries)
|
||||
}
|
||||
|
||||
/// Retrieves a source content as a [`String`] given a [`ContractId`] and path.
|
||||
pub fn get_source_content(
|
||||
&self,
|
||||
contract_id: &ContractId,
|
||||
source_path: &str,
|
||||
) -> Result<Option<String>> {
|
||||
let key = format!("{}/{}", contract_id, source_path);
|
||||
match self.source_code.get(key.as_bytes())? {
|
||||
Some(ivec) => Ok(Some(String::from_utf8(ivec.to_vec()).map_err(|e| {
|
||||
Error::Custom(format!(
|
||||
"[get_source_content] Failed to retrieve source content: {e:?}"
|
||||
))
|
||||
})?)),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds contract source code [`ContractId`] and `Vec` of [`ContractSourceFile`]s to the store,
|
||||
/// deleting existing source associated with the provided contract id before doing so.
|
||||
///
|
||||
/// Delegates operation to [`ContractMetadataStoreOverlay::insert_source`],
|
||||
/// whose documentation provides more details.
|
||||
pub fn insert_source(
|
||||
&self,
|
||||
contract_id: &ContractId,
|
||||
source: &[ContractSourceFile],
|
||||
) -> Result<()> {
|
||||
let existing_source_paths = self.get_source_paths(contract_id)?;
|
||||
let overlay = ContractMetadataStoreOverlay::new(self.sled_db.clone())?;
|
||||
overlay.insert_source(contract_id, source, Some(&existing_source_paths))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Adds contract metadata using provided [`ContractId`] and [`ContractMetaData`] pairs to the store.
|
||||
///
|
||||
/// Delegates operation to [`ContractMetadataStoreOverlay::insert_metadata`], whose documentation
|
||||
/// provides more details.
|
||||
pub fn insert_metadata(
|
||||
&self,
|
||||
contract_ids: &[ContractId],
|
||||
metadata: &[ContractMetaData],
|
||||
) -> Result<()> {
|
||||
let overlay = ContractMetadataStoreOverlay::new(self.sled_db.clone())?;
|
||||
overlay.insert_metadata(contract_ids, metadata)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// The `ContractMetadataStoreOverlay` provides write operations for managing contract metadata in
|
||||
/// underlying sled database. It supports inserting new [`ContractMetaData`] and contract source code
|
||||
/// [`ContractSourceFile`] content and deleting existing source code.
|
||||
struct ContractMetadataStoreOverlay {
|
||||
/// Pointer to the overlay used for accessing and performing database write operations on the store.
|
||||
overlay: SledDbOverlayPtr,
|
||||
}
|
||||
|
||||
impl ContractMetadataStoreOverlay {
|
||||
/// Instantiate a [`ContractMetadataStoreOverlay`] over the provided [`sled::Db`] instance.
|
||||
pub fn new(db: sled::Db) -> Result<Self> {
|
||||
// Create overlay pointer
|
||||
let overlay = Arc::new(Mutex::new(SledDbOverlay::new(&db, vec![])));
|
||||
Ok(Self { overlay: overlay.clone() })
|
||||
}
|
||||
|
||||
/// Inserts [`ContractSourceFile`]s associated with provided [`ContractId`] into the store's
|
||||
/// [`SLED_CONTRACT_SOURCE_CODE_TREE`], committing the changes upon success.
|
||||
///
|
||||
/// This function locks the overlay, then inserts the provided source files into the store while
|
||||
/// handling serialization and potential errors. The provided contract ID is used to create a key
|
||||
/// for each source file by prepending the contract ID to each source code path. On success, the
|
||||
/// contract source code is persisted and made available for use.
|
||||
///
|
||||
/// If optional `source_paths_to_delete` is provided, the function first deletes the existing
|
||||
/// source code associated with these paths before inserting the provided source code.
|
||||
pub fn insert_source(
|
||||
&self,
|
||||
contract_id: &ContractId,
|
||||
source: &[ContractSourceFile],
|
||||
source_paths_to_delete: Option<&[String]>,
|
||||
) -> Result<()> {
|
||||
// Obtain lock
|
||||
let mut lock = self.lock(SLED_CONTRACT_SOURCE_CODE_TREE)?;
|
||||
|
||||
// Delete existing source when existing paths are provided
|
||||
if let Some(paths_to_delete) = source_paths_to_delete {
|
||||
self.delete_source(contract_id, paths_to_delete, &mut lock)?;
|
||||
};
|
||||
|
||||
// Insert each source code file
|
||||
for source_file in source.iter() {
|
||||
// Create key by pre-pending contract id to the source code path
|
||||
let key = format!("{}/{}", contract_id, source_file.path);
|
||||
// Insert the source code
|
||||
lock.insert(
|
||||
SLED_CONTRACT_SOURCE_CODE_TREE,
|
||||
key.as_bytes(),
|
||||
source_file.content.as_bytes(),
|
||||
)?;
|
||||
info!(target: "explorerd::contract_meta_store::insert_source", "Inserted contract source for path {}", key);
|
||||
}
|
||||
|
||||
// Commit the changes
|
||||
lock.apply()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Deletes source code associated with provided [`ContractId`] from the store's [`SLED_CONTRACT_SOURCE_CODE_TREE`],
|
||||
/// committing the changes upon success.
|
||||
///
|
||||
/// This auxiliary function locks the overlay, then removes the code associated with the provided
|
||||
/// contract ID from the store, handling serialization and potential errors. The contract ID is
|
||||
/// prepended to each source code path to create the keys for deletion. On success, the contract
|
||||
/// source code is permanently deleted.
|
||||
fn delete_source(
|
||||
&self,
|
||||
contract_id: &ContractId,
|
||||
source_paths: &[String],
|
||||
lock: &mut MutexGuard<SledDbOverlay>,
|
||||
) -> Result<()> {
|
||||
// Delete each source file associated with provided paths
|
||||
for path in source_paths.iter() {
|
||||
// Create key by pre-pending contract id to the source code path
|
||||
let key = format!("{}/{}", contract_id, path);
|
||||
// Delete the source code
|
||||
lock.remove(SLED_CONTRACT_SOURCE_CODE_TREE, key.as_bytes())?;
|
||||
debug!(target: "explorerd::contract_meta_store::delete_source", "Deleted contract source for path {}", key);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Inserts [`ContractId`] and [`ContractMetaData`] pairs into the store's [`SLED_CONTRACT_METADATA_TREE`],
|
||||
/// committing the changes upon success.
|
||||
///
|
||||
/// This function locks the overlay, verifies that the contract_ids and metadata arrays have matching lengths,
|
||||
/// then inserts them into the store while handling serialization and potential errors. On success,
|
||||
/// contract metadata is persisted and available for use.
|
||||
pub fn insert_metadata(
|
||||
&self,
|
||||
contract_ids: &[ContractId],
|
||||
metadata: &[ContractMetaData],
|
||||
) -> Result<()> {
|
||||
let mut lock = self.lock(SLED_CONTRACT_METADATA_TREE)?;
|
||||
|
||||
// Ensure lengths of contract_ids and metadata arrays match
|
||||
if contract_ids.len() != metadata.len() {
|
||||
return Err(Error::Custom(String::from(
|
||||
"The lengths of contract_ids and metadata arrays must match",
|
||||
)));
|
||||
}
|
||||
|
||||
// Insert each contract id and metadata pair
|
||||
for (contract_id, metadata) in contract_ids.iter().zip(metadata.iter()) {
|
||||
// Serialize the gas data
|
||||
let serialized_metadata = serialize(metadata);
|
||||
|
||||
// Insert serialized gas data
|
||||
lock.insert(
|
||||
SLED_CONTRACT_METADATA_TREE,
|
||||
contract_id.to_string().as_bytes(),
|
||||
&serialized_metadata,
|
||||
)?;
|
||||
info!(target: "explorerd::contract_meta_store::insert_metadata",
|
||||
"Inserted contract metadata for contract_id {}: {metadata:?}", contract_id.to_string());
|
||||
}
|
||||
|
||||
// Commit the changes
|
||||
lock.apply()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Acquires a lock on the database, opening a specified tree for write operations, returning a
|
||||
/// [`MutexGuard<SledDbOverlay>`] representing the locked state.
|
||||
pub fn lock(&self, tree_name: &[u8]) -> Result<MutexGuard<SledDbOverlay>> {
|
||||
// Lock the database, open tree, and return lock
|
||||
let mut lock = self.overlay.lock().unwrap();
|
||||
lock.open_tree(tree_name, true)?;
|
||||
Ok(lock)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
/// This test module verifies the correct insertion and retrieval of contract metadata and source code.
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test_utils::init_logger;
|
||||
use darkfi_sdk::crypto::MONEY_CONTRACT_ID;
|
||||
use sled_overlay::sled::Config;
|
||||
|
||||
// Test source paths data
|
||||
const TEST_SOURCE_PATHS: &[&str] = &["test/source1.rs", "test/source2.rs"];
|
||||
|
||||
// Test source code data
|
||||
const TEST_SOURCE_CONTENT: &[&str] =
|
||||
&["fn main() { println!(\"Hello, world!\"); }", "fn add(a: i32, b: i32) -> i32 { a + b }"];
|
||||
|
||||
/// Tests the storing of contract source code by setting up the store, retrieving loaded source paths
|
||||
/// and verifying that the retrieved paths match against expected results.
|
||||
#[test]
|
||||
fn test_add_contract_source() -> Result<()> {
|
||||
// Setup test, returning initialized contract metadata store
|
||||
let store = setup()?;
|
||||
|
||||
// Load source code tests data
|
||||
let contract_id = load_source_code(&store)?;
|
||||
|
||||
// Initialize expected source paths
|
||||
let expected_source_paths: Vec<String> =
|
||||
TEST_SOURCE_PATHS.iter().map(|s| s.to_string()).collect();
|
||||
|
||||
// Retrieve actual loaded source files
|
||||
let actual_source_paths = store.get_source_paths(contract_id)?;
|
||||
|
||||
// Verify that loaded source code matches expected results
|
||||
assert_eq!(expected_source_paths, actual_source_paths);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Validates the retrieval of a contract source file from the metadata store by setting up the store,
|
||||
/// loading test source code data, and verifying that loaded source contents match against
|
||||
/// expected content.
|
||||
#[test]
|
||||
fn test_get_contract_source() -> Result<()> {
|
||||
// Setup test, returning initialized contract metadata store
|
||||
let store = setup()?;
|
||||
|
||||
// Load source code tests data
|
||||
let contract_id = load_source_code(&store)?;
|
||||
|
||||
// Iterate through test data
|
||||
for (source_path, expected_content) in
|
||||
TEST_SOURCE_PATHS.iter().zip(TEST_SOURCE_CONTENT.iter())
|
||||
{
|
||||
// Get the content of the source path from the store
|
||||
let actual_source = store.get_source_content(contract_id, source_path)?;
|
||||
|
||||
// Verify that the source code content is the store
|
||||
assert!(actual_source.is_some(), "No content found for path: {}", source_path);
|
||||
|
||||
// Validate that the source content matches expected results
|
||||
assert_eq!(
|
||||
actual_source.unwrap(),
|
||||
expected_content.to_string(),
|
||||
"Actual source does not match the expected results for path: {}",
|
||||
source_path
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests the addition of [`ContractMetaData`] to the store by setting up the store, inserting
|
||||
/// metadata, and verifying the inserted data matches the expected results.
|
||||
#[test]
|
||||
fn test_add_metadata() -> Result<()> {
|
||||
// Setup test, returning initialized contract metadata store
|
||||
let store = setup()?;
|
||||
|
||||
// Unique identifier for contracts in tests
|
||||
let contract_id: ContractId = *MONEY_CONTRACT_ID;
|
||||
|
||||
// Declare expected metadata used for test
|
||||
let expected_metadata: ContractMetaData = ContractMetaData::new(
|
||||
"Money Contract".to_string(),
|
||||
"Money Contract Description".to_string(),
|
||||
);
|
||||
|
||||
// Add metadata for the source code to the test
|
||||
store.insert_metadata(&[contract_id], &[expected_metadata.clone()])?;
|
||||
|
||||
// Get the metadata content from the store
|
||||
let actual_metadata = store.get(&contract_id)?;
|
||||
|
||||
// Verify that the metadata exists in the store
|
||||
assert!(actual_metadata.is_some());
|
||||
|
||||
// Verify actual metadata matches expected results
|
||||
assert_eq!(actual_metadata.unwrap(), expected_metadata.clone());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sets up a test case for contract metadata store testing by initializing the logger
|
||||
/// and returning an initialized [`ContractMetaStore`].
|
||||
fn setup() -> Result<ContractMetaStore> {
|
||||
// Initialize logger to show execution output
|
||||
init_logger(simplelog::LevelFilter::Off, vec!["sled", "runtime", "net"]);
|
||||
|
||||
// Initialize an in-memory sled db instance
|
||||
let db = Config::new().temporary(true).open()?;
|
||||
|
||||
// Initialize the contract store
|
||||
ContractMetaStore::new(&db)
|
||||
}
|
||||
|
||||
/// Loads [`TEST_SOURCE_PATHS`] and [`TEST_SOURCE_CONTENT`] into the provided
|
||||
/// [`ContractMetaStore`] to test source code insertion and retrieval.
|
||||
fn load_source_code(store: &ContractMetaStore) -> Result<&'static ContractId> {
|
||||
// Define the contract ID for testing
|
||||
let contract_id = &MONEY_CONTRACT_ID;
|
||||
|
||||
// Define sample source files for testing using the shared paths and content
|
||||
let test_sources: Vec<ContractSourceFile> = TEST_SOURCE_PATHS
|
||||
.iter()
|
||||
.zip(TEST_SOURCE_CONTENT.iter())
|
||||
.map(|(path, content)| ContractSourceFile::new(path.to_string(), content.to_string()))
|
||||
.collect();
|
||||
|
||||
// Add test source code to the store
|
||||
store.insert_source(contract_id, &test_sources)?;
|
||||
|
||||
Ok(contract_id)
|
||||
}
|
||||
}
|
||||
@@ -1,516 +0,0 @@
|
||||
/* This file is part of DarkFi (https://dark.fi)
|
||||
*
|
||||
* Copyright (C) 2020-2025 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/>.
|
||||
*/
|
||||
|
||||
use std::io::{Cursor, Read};
|
||||
|
||||
use tar::Archive;
|
||||
use tinyjson::JsonValue;
|
||||
|
||||
use darkfi::{Error, Result};
|
||||
use darkfi_sdk::crypto::{ContractId, DAO_CONTRACT_ID, DEPLOYOOOR_CONTRACT_ID, MONEY_CONTRACT_ID};
|
||||
use darkfi_serial::deserialize;
|
||||
|
||||
use crate::{
|
||||
contract_meta_store::{ContractMetaData, ContractSourceFile},
|
||||
ExplorerService,
|
||||
};
|
||||
|
||||
/// Represents a contract record embellished with details that are not stored on-chain.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ContractRecord {
|
||||
/// The Contract ID as a string
|
||||
pub id: String,
|
||||
|
||||
/// The optional name of the contract
|
||||
pub name: Option<String>,
|
||||
|
||||
/// The optional description of the contract
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
impl ContractRecord {
|
||||
/// Auxiliary function to convert a `ContractRecord` into a `JsonValue` array.
|
||||
pub fn to_json_array(&self) -> JsonValue {
|
||||
JsonValue::Array(vec![
|
||||
JsonValue::String(self.id.clone()),
|
||||
JsonValue::String(self.name.clone().unwrap_or_default()),
|
||||
JsonValue::String(self.description.clone().unwrap_or_default()),
|
||||
])
|
||||
}
|
||||
}
|
||||
impl ExplorerService {
|
||||
/// Retrieves all contracts from the store excluding native contracts (DAO, Deployooor, and Money),
|
||||
/// transforming them into `Vec` of [`ContractRecord`]s, and returns the result.
|
||||
pub fn get_contracts(&self) -> Result<Vec<ContractRecord>> {
|
||||
let native_contracts = [*DAO_CONTRACT_ID, *DEPLOYOOOR_CONTRACT_ID, *MONEY_CONTRACT_ID];
|
||||
self.get_filtered_contracts(|contract_id| !native_contracts.contains(contract_id))
|
||||
}
|
||||
|
||||
/// Retrieves all native contracts (DAO, Deployooor, and Money) from the store, transforming them
|
||||
/// into `Vec` of [`ContractRecord`]s and returns the result.
|
||||
pub fn get_native_contracts(&self) -> Result<Vec<ContractRecord>> {
|
||||
let native_contracts = [*DAO_CONTRACT_ID, *DEPLOYOOOR_CONTRACT_ID, *MONEY_CONTRACT_ID];
|
||||
self.get_filtered_contracts(|contract_id| native_contracts.contains(contract_id))
|
||||
}
|
||||
|
||||
/// Fetches a list of source code file paths for a given [ContractId], returning an empty vector
|
||||
/// if no contracts are found.
|
||||
pub fn get_contract_source_paths(&self, contract_id: &ContractId) -> Result<Vec<String>> {
|
||||
self.db.contract_meta_store.get_source_paths(contract_id).map_err(|e| {
|
||||
Error::DatabaseError(format!(
|
||||
"[get_contract_source_paths] Retrieval of contract source code paths failed: {e:?}"
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
/// Fetches [`ContractMetaData`] for a given [`ContractId`], returning `None` if no metadata is found.
|
||||
pub fn get_contract_metadata(
|
||||
&self,
|
||||
contract_id: &ContractId,
|
||||
) -> Result<Option<ContractMetaData>> {
|
||||
self.db.contract_meta_store.get(contract_id).map_err(|e| {
|
||||
Error::DatabaseError(format!(
|
||||
"[get_contract_metadata] Retrieval of contract metadata paths failed: {e:?}"
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
/// Fetches the source code content for a specified [`ContractId`] and `path`, returning `None` if
|
||||
/// no source content is found.
|
||||
pub fn get_contract_source_content(
|
||||
&self,
|
||||
contract_id: &ContractId,
|
||||
path: &str,
|
||||
) -> Result<Option<String>> {
|
||||
self.db.contract_meta_store.get_source_content(contract_id, path).map_err(|e| {
|
||||
Error::DatabaseError(format!(
|
||||
"[get_contract_source_content] Retrieval of contract source file failed: {e:?}"
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
/// Fetches the total contract count of all deployed contracts in the explorer database.
|
||||
pub fn get_contract_count(&self) -> usize {
|
||||
self.db.blockchain.contracts.wasm.len()
|
||||
}
|
||||
|
||||
/// Adds source code for a specified [`ContractId`] from a provided tar file (in bytes).
|
||||
///
|
||||
/// This function extracts the tar archive from `tar_bytes`, then loads each source file
|
||||
/// into the store. Each file is keyed by its path prefixed with the Contract ID.
|
||||
/// Returns a successful result or an error.
|
||||
pub fn add_contract_source(&self, contract_id: &ContractId, tar_bytes: &[u8]) -> Result<()> {
|
||||
// Untar the source code
|
||||
let source = untar_source(tar_bytes)?;
|
||||
|
||||
// Insert contract source code
|
||||
self.db.contract_meta_store.insert_source(contract_id, &source).map_err(|e| {
|
||||
Error::DatabaseError(format!(
|
||||
"[add_contract_source] Adding of contract source code failed: {e:?}"
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
/// Adds provided [`ContractId`] with corresponding [`ContractMetaData`] pairs into the contract
|
||||
/// metadata store, returning a successful result upon success.
|
||||
pub fn add_contract_metadata(
|
||||
&self,
|
||||
contract_ids: &[ContractId],
|
||||
metadata: &[ContractMetaData],
|
||||
) -> Result<()> {
|
||||
self.db.contract_meta_store.insert_metadata(contract_ids, metadata).map_err(|e| {
|
||||
Error::DatabaseError(format!(
|
||||
"[add_contract_metadata] Upload of contract source code failed: {e:?}"
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
/// Converts a [`ContractId`] into a [`ContractRecord`].
|
||||
///
|
||||
/// This function retrieves the [`ContractMetaData`] associated with the provided Contract ID
|
||||
/// and uses any found metadata to construct a contract record. Upon success, the function
|
||||
/// returns a [`ContractRecord`] containing relevant details about the contract.
|
||||
fn to_contract_record(&self, contract_id: &ContractId) -> Result<ContractRecord> {
|
||||
let metadata = self.db.contract_meta_store.get(contract_id)?;
|
||||
let name: Option<String>;
|
||||
let description: Option<String>;
|
||||
|
||||
// Set name and description based on the presence of metadata
|
||||
if let Some(metadata) = metadata {
|
||||
name = Some(metadata.name);
|
||||
description = Some(metadata.description);
|
||||
} else {
|
||||
name = None;
|
||||
description = None;
|
||||
}
|
||||
|
||||
// Return transformed contract record
|
||||
Ok(ContractRecord { id: contract_id.to_string(), name, description })
|
||||
}
|
||||
|
||||
/// Auxiliary function that retrieves [`ContractRecord`]s filtered by a provided `filter_fn` closure.
|
||||
///
|
||||
/// This function accepts a filter function `Fn(&ContractId) -> bool` that determines
|
||||
/// which contracts are included based on their [`ContractId`]. It iterates over
|
||||
/// Contract IDs stored in the blockchain's contract tree, applying the filter function to decide inclusion.
|
||||
/// Converts the filtered Contract IDs into [`ContractRecord`] instances, returning them as a `Vec`,
|
||||
/// or an empty `Vec` if no contracts are found.
|
||||
fn get_filtered_contracts<F>(&self, filter_fn: F) -> Result<Vec<ContractRecord>>
|
||||
where
|
||||
F: Fn(&ContractId) -> bool,
|
||||
{
|
||||
let contract_keys = self.db.blockchain.contracts.wasm.iter().keys();
|
||||
|
||||
// Iterate through stored Contract IDs, filtering out the contracts based filter
|
||||
contract_keys
|
||||
.filter_map(|serialized_contract_id| {
|
||||
// Deserialize the serialized Contract ID
|
||||
let contract_id: ContractId = match serialized_contract_id
|
||||
.map_err(Error::from)
|
||||
.and_then(|id_bytes| deserialize(&id_bytes).map_err(Error::from))
|
||||
{
|
||||
Ok(id) => id,
|
||||
Err(e) => {
|
||||
return Some(Err(Error::DatabaseError(format!(
|
||||
"[get_filtered_contracts] Contract ID retrieval or deserialization failed: {e:?}"
|
||||
))));
|
||||
}
|
||||
};
|
||||
|
||||
// Apply the filter
|
||||
if filter_fn(&contract_id) {
|
||||
// Convert the matching Contract ID into a `ContractRecord`, return result
|
||||
return match self.to_contract_record(&contract_id).map_err(|e| {
|
||||
Error::DatabaseError(format!("[get_filtered_contracts] Failed to convert contract: {e:?}"))
|
||||
}) {
|
||||
Ok(record) => Some(Ok(record)),
|
||||
Err(e) => Some(Err(e)),
|
||||
};
|
||||
}
|
||||
|
||||
// Skip contracts that do not match the filter
|
||||
None
|
||||
})
|
||||
.collect::<Result<Vec<ContractRecord>>>()
|
||||
}
|
||||
}
|
||||
|
||||
/// Auxiliary function that extracts source code files from a TAR archive provided as a byte slice [`&[u8]`],
|
||||
/// returning a `Vec` of [`ContractSourceFile`]s representing the extracted file paths and their contents.
|
||||
pub fn untar_source(tar_bytes: &[u8]) -> Result<Vec<ContractSourceFile>> {
|
||||
// Use a Cursor and archive to read the tar file
|
||||
let cursor = Cursor::new(tar_bytes);
|
||||
let mut archive = Archive::new(cursor);
|
||||
|
||||
// Vectors to hold the source paths and source contents
|
||||
let mut source: Vec<ContractSourceFile> = Vec::new();
|
||||
|
||||
// Iterate through the entries in the tar archive
|
||||
for tar_entry in archive.entries()? {
|
||||
let mut tar_entry = tar_entry?;
|
||||
let path = tar_entry.path()?.to_path_buf();
|
||||
|
||||
// Check if the entry is a file
|
||||
if tar_entry.header().entry_type().is_file() {
|
||||
let mut content = Vec::new();
|
||||
tar_entry.read_to_end(&mut content)?;
|
||||
|
||||
// Convert the contents into a string
|
||||
let source_content = String::from_utf8(content)
|
||||
.map_err(|_| Error::ParseFailed("Failed converting source code to a string"))?;
|
||||
|
||||
// Collect source paths and contents
|
||||
let path_str = path.to_string_lossy().into_owned();
|
||||
source.push(ContractSourceFile::new(path_str, source_content));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(source)
|
||||
}
|
||||
|
||||
/// This test module ensures the correctness of the [`ExplorerService`] functionality with
|
||||
/// respect to smart contracts.
|
||||
///
|
||||
/// The tests in this module cover adding, loading, storing, retrieving, and validating contract
|
||||
/// metadata and source code. The primary goal is to validate the accuracy and reliability of
|
||||
/// the `ExplorerService` when handling contract-related operations.
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{fs::File, io::Read, path::Path};
|
||||
|
||||
use tar::Archive;
|
||||
use tempdir::TempDir;
|
||||
|
||||
use darkfi::Error::Custom;
|
||||
use darkfi_sdk::crypto::MONEY_CONTRACT_ID;
|
||||
|
||||
use super::*;
|
||||
use crate::test_utils::init_logger;
|
||||
|
||||
/// Tests the adding of [`ContractMetaData`] to the store by adding
|
||||
/// metadata, and verifying the inserted data matches the expected results.
|
||||
#[test]
|
||||
fn test_add_metadata() -> Result<()> {
|
||||
// Setup test, returning initialized service
|
||||
let service = setup()?;
|
||||
|
||||
// Unique identifier for contracts in tests
|
||||
let contract_id: ContractId = *MONEY_CONTRACT_ID;
|
||||
|
||||
// Declare expected metadata used for test
|
||||
let expected_metadata: ContractMetaData = ContractMetaData::new(
|
||||
"Money Contract".to_string(),
|
||||
"Money Contract Description".to_string(),
|
||||
);
|
||||
|
||||
// Add the metadata
|
||||
service.add_contract_metadata(&[contract_id], &[expected_metadata.clone()])?;
|
||||
|
||||
// Get the metadata that was loaded as actual results
|
||||
let actual_metadata = service.get_contract_metadata(&contract_id)?;
|
||||
|
||||
// Verify existence of loaded metadata
|
||||
assert!(actual_metadata.is_some());
|
||||
|
||||
// Confirm actual metadata match expected results
|
||||
assert_eq!(actual_metadata.unwrap(), expected_metadata.clone());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// This test validates the loading and retrieval of native contract metadata. It sets up the
|
||||
/// explorer service, loads native contract metadata, and then verifies metadata retrieval
|
||||
/// for each native contract.
|
||||
#[test]
|
||||
fn test_load_native_contract_metadata() -> Result<()> {
|
||||
// Setup test, returning initialized service
|
||||
let service = setup()?;
|
||||
|
||||
// Load native contract metadata
|
||||
service.load_native_contract_metadata()?;
|
||||
|
||||
// Define Contract IDs used to retrieve loaded metadata
|
||||
let native_contract_ids = [*DAO_CONTRACT_ID, *DEPLOYOOOR_CONTRACT_ID, *MONEY_CONTRACT_ID];
|
||||
|
||||
// For each native contract, verify metadata was loaded
|
||||
for contract_id in native_contract_ids.iter() {
|
||||
let metadata = service.get_contract_metadata(contract_id)?;
|
||||
assert!(metadata.is_some());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// This test validates the loading, storage, and retrieval of native contract source code. It sets up the
|
||||
/// explorer service, loads native contract sources, and then verifies both the source paths and content
|
||||
/// for each native contract. The test compares the retrieved source paths and content against the expected
|
||||
/// results from the corresponding tar archives.
|
||||
#[test]
|
||||
fn test_load_native_contracts() -> Result<()> {
|
||||
// Setup test, returning initialized service
|
||||
let service = setup()?;
|
||||
|
||||
// Load native contracts
|
||||
service.load_native_contract_sources()?;
|
||||
|
||||
// Define contract archive paths
|
||||
let native_contract_tars = [
|
||||
"native_contracts_src/dao_contract_src.tar",
|
||||
"native_contracts_src/deployooor_contract_src.tar",
|
||||
"native_contracts_src/money_contract_src.tar",
|
||||
];
|
||||
|
||||
// Define Contract IDs to associate with each contract source archive
|
||||
let native_contract_ids = [*DAO_CONTRACT_ID, *DEPLOYOOOR_CONTRACT_ID, *MONEY_CONTRACT_ID];
|
||||
|
||||
// Iterate archive and verify actual match expected results
|
||||
for (&tar_file, &contract_id) in native_contract_tars.iter().zip(&native_contract_ids) {
|
||||
// Verify that source paths match
|
||||
verify_source_paths(&service, tar_file, contract_id)?;
|
||||
|
||||
// Verify that source content match
|
||||
verify_source_content(&service, tar_file, contract_id)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// This test validates the transformation of a [`ContractId`] into a [`ContractRecord`].
|
||||
/// It sets up the explorer service, adds test metadata for a specific Contract ID, and then verifies the
|
||||
/// correct transformation of this Contract ID into a ContractRecord.
|
||||
#[test]
|
||||
fn test_to_contract_record() -> Result<()> {
|
||||
// Setup test, returning initialized service
|
||||
let service = setup()?;
|
||||
|
||||
// Unique identifier for contracts in tests
|
||||
let contract_id: ContractId = *MONEY_CONTRACT_ID;
|
||||
|
||||
// Declare expected metadata used for test
|
||||
let expected_metadata: ContractMetaData = ContractMetaData::new(
|
||||
"Money Contract".to_string(),
|
||||
"Money Contract Description".to_string(),
|
||||
);
|
||||
|
||||
// Load contract metadata used for test
|
||||
service.add_contract_metadata(&[contract_id], &[expected_metadata.clone()])?;
|
||||
|
||||
// Transform Contract ID to a `ContractRecord`
|
||||
let contract_record = service.to_contract_record(&contract_id)?;
|
||||
|
||||
// Verify that name and description exist
|
||||
assert!(
|
||||
contract_record.name.is_some(),
|
||||
"Expected to_contract_record to return a contract with name"
|
||||
);
|
||||
assert!(
|
||||
contract_record.description.is_some(),
|
||||
"Expected to_contract_record to return a contract with description"
|
||||
);
|
||||
|
||||
// Verify that id, name, and description match expected results
|
||||
assert_eq!(contract_id.to_string(), contract_record.id);
|
||||
assert_eq!(expected_metadata.name, contract_record.name.unwrap());
|
||||
assert_eq!(expected_metadata.description, contract_record.description.unwrap());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sets up a test case for contract metadata store testing by initializing the logger
|
||||
/// and returning an initialized [`ExplorerService`].
|
||||
fn setup() -> Result<ExplorerService> {
|
||||
// Initialize logger to show execution output
|
||||
init_logger(simplelog::LevelFilter::Off, vec!["sled", "runtime", "net"]);
|
||||
|
||||
// Create a temporary directory for sled DB
|
||||
let temp_dir = TempDir::new("test")?;
|
||||
|
||||
// Initialize a sled DB instance using the temporary directory's path
|
||||
let db_path = temp_dir.path().join("sled_db");
|
||||
|
||||
// Initialize the explorer service
|
||||
ExplorerService::new(db_path.to_string_lossy().into_owned())
|
||||
}
|
||||
|
||||
/// This Auxiliary function verifies that the loaded native contract source paths match the expected results
|
||||
/// from a given contract archive. This function extracts source paths from the specified `tar_file`, retrieves
|
||||
/// the actual paths for the [`ContractId`] from the ExplorerService, and compares them to ensure they match.
|
||||
fn verify_source_paths(
|
||||
service: &ExplorerService,
|
||||
tar_file: &str,
|
||||
contract_id: ContractId,
|
||||
) -> Result<()> {
|
||||
// Read the tar file and extract source paths
|
||||
let tar_bytes = std::fs::read(tar_file)?;
|
||||
let mut expected_source_paths = extract_file_paths_from_tar(&tar_bytes)?;
|
||||
|
||||
// Retrieve and sort actual source paths for the provided Contract ID
|
||||
let mut actual_source_paths = service.get_contract_source_paths(&contract_id)?;
|
||||
|
||||
// Sort paths to ensure they are in the same order needed for assert
|
||||
expected_source_paths.sort();
|
||||
actual_source_paths.sort();
|
||||
|
||||
// Verify actual source matches expected result
|
||||
assert_eq!(
|
||||
expected_source_paths, actual_source_paths,
|
||||
"Mismatch between expected and actual source paths for tar file: {}",
|
||||
tar_file
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// This auxiliary function verifies that the loaded native contract source content matches the
|
||||
/// expected results from a given contract source archive. It extracts source files from the specified
|
||||
/// `tar_file`, retrieves the actual content for each file path using the [`ContractId`] from the
|
||||
/// ExplorerService, and compares them to ensure the content match.
|
||||
fn verify_source_content(
|
||||
service: &ExplorerService,
|
||||
tar_file: &str,
|
||||
contract_id: ContractId,
|
||||
) -> Result<()> {
|
||||
// Read the tar file
|
||||
let tar_bytes = std::fs::read(tar_file)?;
|
||||
let expected_source_paths = extract_file_paths_from_tar(&tar_bytes)?;
|
||||
|
||||
// Validate contents of tar archive source code content
|
||||
for file_path in expected_source_paths {
|
||||
// Get the source code content
|
||||
let actual_source = service.get_contract_source_content(&contract_id, &file_path)?;
|
||||
|
||||
// Verify source content exists
|
||||
assert!(
|
||||
actual_source.is_some(),
|
||||
"Actual source `{}` is missing in the store.",
|
||||
file_path
|
||||
);
|
||||
|
||||
// Read the source content from the tar archive
|
||||
let expected_source = read_file_from_tar(tar_file, &file_path)?;
|
||||
|
||||
// Verify actual source matches expected results
|
||||
assert_eq!(
|
||||
actual_source.unwrap(),
|
||||
expected_source,
|
||||
"Actual source does not match expected results `{}`.",
|
||||
file_path
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Auxiliary function that reads the contents of specified `file_path` within a tar archive.
|
||||
fn read_file_from_tar(tar_path: &str, file_path: &str) -> Result<String> {
|
||||
let file = File::open(tar_path)?;
|
||||
let mut archive = Archive::new(file);
|
||||
for entry in archive.entries()? {
|
||||
let mut entry = entry?;
|
||||
if let Ok(path) = entry.path() {
|
||||
if path == Path::new(file_path) {
|
||||
let mut content = String::new();
|
||||
entry.read_to_string(&mut content)?;
|
||||
return Ok(content);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(Custom(format!("File {} not found in tar archive.", file_path)))
|
||||
}
|
||||
|
||||
/// Auxiliary function that extracts all file paths from the given `tar_bytes` tar archive.
|
||||
pub fn extract_file_paths_from_tar(tar_bytes: &[u8]) -> Result<Vec<String>> {
|
||||
let cursor = Cursor::new(tar_bytes);
|
||||
let mut archive = Archive::new(cursor);
|
||||
|
||||
// Collect paths from the tar archive
|
||||
let mut file_paths = Vec::new();
|
||||
for entry in archive.entries()? {
|
||||
let entry = entry?;
|
||||
let path = entry.path()?;
|
||||
|
||||
// Skip directories and only include files
|
||||
if entry.header().entry_type().is_file() {
|
||||
file_paths.push(path.to_string_lossy().to_string());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(file_paths)
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
/* This file is part of DarkFi (https://dark.fi)
|
||||
*
|
||||
* Copyright (C) 2020-2025 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/>.
|
||||
*/
|
||||
|
||||
use log::error;
|
||||
|
||||
use darkfi::{
|
||||
rpc::jsonrpc::{ErrorCode::ServerError, JsonError, JsonResult},
|
||||
Error,
|
||||
};
|
||||
|
||||
/// Custom RPC errors available for blockchain explorer.
|
||||
/// Please sort them sensefully.
|
||||
pub enum RpcError {
|
||||
// Misc errors
|
||||
PingFailed = -32300,
|
||||
}
|
||||
|
||||
fn to_tuple(e: RpcError) -> (i32, String) {
|
||||
let msg = match e {
|
||||
// Misc errors
|
||||
RpcError::PingFailed => "Darkfid daemon ping error",
|
||||
};
|
||||
|
||||
(e as i32, msg.to_string())
|
||||
}
|
||||
|
||||
pub fn server_error(e: RpcError, id: u16, msg: Option<&str>) -> JsonResult {
|
||||
let (code, default_msg) = to_tuple(e);
|
||||
|
||||
if let Some(message) = msg {
|
||||
return JsonError::new(ServerError(code), Some(message.to_string()), id).into()
|
||||
}
|
||||
|
||||
JsonError::new(ServerError(code), Some(default_msg), id).into()
|
||||
}
|
||||
|
||||
/// Handles a database error by formatting the output, logging it with target-specific context,
|
||||
/// and returning a [`DatabaseError`].
|
||||
pub fn handle_database_error(target: &str, message: &str, error: impl std::fmt::Debug) -> Error {
|
||||
let error_message = format!("{}: {:?}", message, error);
|
||||
let formatted_target = format!("blockchain-explorer:: {target}");
|
||||
error!(target: &formatted_target, "{}", error_message);
|
||||
Error::DatabaseError(error_message)
|
||||
}
|
||||
@@ -1,384 +0,0 @@
|
||||
/* This file is part of DarkFi (https://dark.fi)
|
||||
*
|
||||
* Copyright (C) 2020-2025 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/>.
|
||||
*/
|
||||
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
str::FromStr,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
use log::{debug, error, info};
|
||||
use rpc_blocks::subscribe_blocks;
|
||||
use sled_overlay::sled;
|
||||
use smol::{lock::Mutex, stream::StreamExt};
|
||||
use structopt_toml::{serde::Deserialize, structopt::StructOpt, StructOptToml};
|
||||
use url::Url;
|
||||
|
||||
use darkfi::{
|
||||
async_daemonize,
|
||||
blockchain::{Blockchain, BlockchainOverlay},
|
||||
cli_desc,
|
||||
rpc::{
|
||||
client::RpcClient,
|
||||
server::{listen_and_serve, RequestHandler},
|
||||
},
|
||||
system::{StoppableTask, StoppableTaskPtr},
|
||||
util::path::expand_path,
|
||||
validator::utils::deploy_native_contracts,
|
||||
Error, Result,
|
||||
};
|
||||
use darkfi_sdk::crypto::{ContractId, DAO_CONTRACT_ID, DEPLOYOOOR_CONTRACT_ID, MONEY_CONTRACT_ID};
|
||||
|
||||
use crate::{
|
||||
contract_meta_store::{ContractMetaData, ContractMetaStore},
|
||||
contracts::untar_source,
|
||||
metrics_store::MetricsStore,
|
||||
};
|
||||
|
||||
/// Crate errors
|
||||
mod error;
|
||||
|
||||
/// JSON-RPC requests handler and methods
|
||||
mod rpc;
|
||||
mod rpc_blocks;
|
||||
mod rpc_contracts;
|
||||
mod rpc_statistics;
|
||||
mod rpc_transactions;
|
||||
|
||||
/// Service functionality related to blocks
|
||||
mod blocks;
|
||||
|
||||
/// Service functionality related to transactions
|
||||
mod transactions;
|
||||
|
||||
/// Service functionality related to statistics
|
||||
mod statistics;
|
||||
|
||||
/// Service functionality related to contracts
|
||||
mod contracts;
|
||||
|
||||
/// Test utilities used for unit and integration testing
|
||||
mod test_utils;
|
||||
|
||||
/// Database store functionality related to metrics
|
||||
mod metrics_store;
|
||||
|
||||
/// Database store functionality related to contract metadata
|
||||
mod contract_meta_store;
|
||||
|
||||
const CONFIG_FILE: &str = "blockchain_explorer_config.toml";
|
||||
const CONFIG_FILE_CONTENTS: &str = include_str!("../blockchain_explorer_config.toml");
|
||||
|
||||
// Load the contract source archives to bootstrap them on explorer startup
|
||||
lazy_static! {
|
||||
static ref NATIVE_CONTRACT_SOURCE_ARCHIVES: HashMap<String, &'static [u8]> = {
|
||||
let mut src_map = HashMap::new();
|
||||
src_map.insert(
|
||||
MONEY_CONTRACT_ID.to_string(),
|
||||
&include_bytes!("../native_contracts_src/money_contract_src.tar")[..],
|
||||
);
|
||||
src_map.insert(
|
||||
DAO_CONTRACT_ID.to_string(),
|
||||
&include_bytes!("../native_contracts_src/dao_contract_src.tar")[..],
|
||||
);
|
||||
src_map.insert(
|
||||
DEPLOYOOOR_CONTRACT_ID.to_string(),
|
||||
&include_bytes!("../native_contracts_src/deployooor_contract_src.tar")[..],
|
||||
);
|
||||
src_map
|
||||
};
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, StructOpt, StructOptToml)]
|
||||
#[serde(default)]
|
||||
#[structopt(name = "blockchain-explorer", about = cli_desc!())]
|
||||
struct Args {
|
||||
#[structopt(short, long)]
|
||||
/// Configuration file to use
|
||||
config: Option<String>,
|
||||
|
||||
#[structopt(short, long, default_value = "tcp://127.0.0.1:14567")]
|
||||
/// JSON-RPC listen URL
|
||||
rpc_listen: Url,
|
||||
|
||||
#[structopt(long, default_value = "~/.local/share/darkfi/blockchain-explorer/daemon.db")]
|
||||
/// Path to daemon database
|
||||
db_path: String,
|
||||
|
||||
#[structopt(long)]
|
||||
/// Reset the database and start syncing from first block
|
||||
reset: bool,
|
||||
|
||||
#[structopt(short, long, default_value = "tcp://127.0.0.1:8340")]
|
||||
/// darkfid JSON-RPC endpoint
|
||||
endpoint: Url,
|
||||
|
||||
#[structopt(short, long)]
|
||||
/// Set log file to output into
|
||||
log: Option<String>,
|
||||
|
||||
#[structopt(short, parse(from_occurrences))]
|
||||
/// Increase verbosity (-vvv supported)
|
||||
verbose: u8,
|
||||
}
|
||||
|
||||
/// Represents the service layer for the Explorer application, bridging the RPC layer and the database.
|
||||
/// It encapsulates explorer business logic and provides a unified interface for core functionalities,
|
||||
/// providing a clear separation of concerns between RPC handling and data management layers.
|
||||
///
|
||||
/// Core functionalities include:
|
||||
///
|
||||
/// - Data Transformation: Converting database data into structured responses suitable for RPC callers.
|
||||
/// - Blocks: Synchronization, retrieval, counting, and management.
|
||||
/// - Contracts: Handling native and user contract data, source code, tar files, and metadata.
|
||||
/// - Metrics: Providing metric-related data over the life of the chain.
|
||||
/// - Transactions: Synchronization, calculating gas data, retrieval, counting, and related block information.
|
||||
pub struct ExplorerService {
|
||||
/// Explorer database instance
|
||||
db: ExplorerDb,
|
||||
}
|
||||
|
||||
impl ExplorerService {
|
||||
/// Creates a new `ExplorerService` instance.
|
||||
pub fn new(db_path: String) -> Result<Self> {
|
||||
// Initialize explorer database
|
||||
let db = ExplorerDb::new(db_path)?;
|
||||
|
||||
Ok(Self { db })
|
||||
}
|
||||
|
||||
/// Initializes the explorer service by deploying native contracts and loading native contract
|
||||
/// source code and metadata required for its operation.
|
||||
pub async fn init(&self) -> Result<()> {
|
||||
self.deploy_native_contracts().await?;
|
||||
self.load_native_contract_sources()?;
|
||||
self.load_native_contract_metadata()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Deploys native contracts required for gas calculation and retrieval.
|
||||
pub async fn deploy_native_contracts(&self) -> Result<()> {
|
||||
let overlay = BlockchainOverlay::new(&self.db.blockchain)?;
|
||||
deploy_native_contracts(&overlay, 10).await?;
|
||||
overlay.lock().unwrap().overlay.lock().unwrap().apply()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Loads native contract source code into the explorer database by extracting it from tar archives
|
||||
/// created during the explorer build process. The extracted source code is associated with
|
||||
/// the corresponding [`ContractId`] for each loaded contract and stored.
|
||||
pub fn load_native_contract_sources(&self) -> Result<()> {
|
||||
// Iterate each native contract source archive
|
||||
for (contract_id_str, archive_bytes) in NATIVE_CONTRACT_SOURCE_ARCHIVES.iter() {
|
||||
// Untar the native contract source code
|
||||
let source_code = untar_source(archive_bytes)?;
|
||||
|
||||
// Parse contract id into a contract id instance
|
||||
let contract_id = &ContractId::from_str(contract_id_str)?;
|
||||
|
||||
// Add source code into the `ContractMetaStore`
|
||||
self.db.contract_meta_store.insert_source(contract_id, &source_code)?;
|
||||
info!("Loaded contract source for contract {}", contract_id_str.to_string());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Loads [`ContractMetaData`] for deployed native contracts into the explorer database by adding descriptive
|
||||
/// information (e.g., name and description) used to display contract details.
|
||||
pub fn load_native_contract_metadata(&self) -> Result<()> {
|
||||
let contract_ids = [*MONEY_CONTRACT_ID, *DAO_CONTRACT_ID, *DEPLOYOOOR_CONTRACT_ID];
|
||||
|
||||
// Create pre-defined native contract metadata
|
||||
let metadatas = [
|
||||
ContractMetaData::new(
|
||||
"Money".to_string(),
|
||||
"Facilitates money transfers, atomic swaps, minting, freezing, and staking of consensus tokens".to_string(),
|
||||
),
|
||||
ContractMetaData::new(
|
||||
"DAO".to_string(),
|
||||
"Provides functionality for Anonymous DAOs".to_string(),
|
||||
),
|
||||
ContractMetaData::new(
|
||||
"Deployoor".to_string(),
|
||||
"Handles non-native smart contract deployments".to_string(),
|
||||
),
|
||||
];
|
||||
|
||||
// Load contract metadata into the `ContractMetaStore`
|
||||
self.db.contract_meta_store.insert_metadata(&contract_ids, &metadatas)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Resets the explorer state to the specified height. If a genesis block height is provided,
|
||||
/// all blocks and transactions are purged from the database. Otherwise, the state is reverted
|
||||
/// to the given height. The explorer metrics are updated to reflect the updated blocks and
|
||||
/// transactions up to the reset height, ensuring consistency. Returns a result indicating
|
||||
/// success or an error if the operation fails.
|
||||
pub fn reset_explorer_state(&self, height: u32) -> Result<()> {
|
||||
debug!(target: "blockchain-explorer::reset_explorer_state", "Resetting explorer state to height: {height}");
|
||||
|
||||
// Check if a genesis block reset or to a specific height
|
||||
match height {
|
||||
// Reset for genesis height 0, purge blocks and transactions
|
||||
0 => {
|
||||
self.reset_blocks()?;
|
||||
self.reset_transactions()?;
|
||||
debug!(target: "blockchain-explorer::reset_explorer_state", "Successfully reset explorer state to accept a new genesis block");
|
||||
}
|
||||
// Reset for all other heights
|
||||
_ => {
|
||||
self.reset_to_height(height)?;
|
||||
debug!(target: "blockchain-explorer::reset_explorer_state", "Successfully reset blocks to height: {height}");
|
||||
}
|
||||
}
|
||||
|
||||
// Reset gas metrics to the specified height to reflect the updated blockchain state
|
||||
self.db.metrics_store.reset_gas_metrics(height)?;
|
||||
debug!(target: "blockchain-explorer::reset_explorer_state", "Successfully reset metrics store to height: {height}");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the explorer database backed by a `sled` database connection, responsible for maintaining
|
||||
/// persistent state required for blockchain exploration. It serves as the core data layer for the Explorer application,
|
||||
/// storing and managing blockchain data, metrics, and contract-related information.
|
||||
pub struct ExplorerDb {
|
||||
/// The main `sled` database connection used for data storage and retrieval
|
||||
pub sled_db: sled::Db,
|
||||
/// Local copy of the Darkfi blockchain used for block synchronization and exploration
|
||||
pub blockchain: Blockchain,
|
||||
/// Store for tracking chain-related metrics
|
||||
pub metrics_store: MetricsStore,
|
||||
/// Store for managing contract metadata, source code, and related data
|
||||
pub contract_meta_store: ContractMetaStore,
|
||||
}
|
||||
|
||||
impl ExplorerDb {
|
||||
/// Creates a new `ExplorerDb` instance
|
||||
pub fn new(db_path: String) -> Result<Self> {
|
||||
let db_path = expand_path(db_path.as_str())?;
|
||||
let sled_db = sled::open(&db_path)?;
|
||||
let blockchain = Blockchain::new(&sled_db)?;
|
||||
let metrics_store = MetricsStore::new(&sled_db)?;
|
||||
let contract_meta_store = ContractMetaStore::new(&sled_db)?;
|
||||
info!(target: "blockchain-explorer", "Initialized explorer database {}: block count: {}, tx count: {}", db_path.display(), blockchain.len(), blockchain.txs_len());
|
||||
Ok(Self { sled_db, blockchain, metrics_store, contract_meta_store })
|
||||
}
|
||||
}
|
||||
|
||||
/// Defines a daemon structure responsible for handling incoming JSON-RPC requests and delegating them
|
||||
/// to the backend layer for processing. It provides a JSON-RPC interface for managing operations related to
|
||||
/// blocks, transactions, contracts, and metrics.
|
||||
///
|
||||
/// Upon startup, the daemon initializes a background task to handle incoming JSON-RPC requests.
|
||||
/// This includes processing operations related to blocks, transactions, contracts, and metrics by
|
||||
/// delegating them to the backend and returning appropriate RPC responses. Additionally, the daemon
|
||||
/// synchronizes blocks from the `darkfid` daemon into the explorer database and subscribes
|
||||
/// to new blocks, ensuring that the local database remains updated in real-time.
|
||||
pub struct Explorerd {
|
||||
/// Explorer service instance
|
||||
pub service: ExplorerService,
|
||||
/// JSON-RPC connection tracker
|
||||
pub rpc_connections: Mutex<HashSet<StoppableTaskPtr>>,
|
||||
/// JSON-RPC client to execute requests to darkfid daemon
|
||||
pub rpc_client: RpcClient,
|
||||
}
|
||||
|
||||
impl Explorerd {
|
||||
/// Creates a new `BlockchainExplorer` instance.
|
||||
async fn new(db_path: String, endpoint: Url, ex: Arc<smol::Executor<'static>>) -> Result<Self> {
|
||||
// Initialize rpc client
|
||||
let rpc_client = RpcClient::new(endpoint.clone(), ex).await?;
|
||||
info!(target: "blockchain-explorer", "Created rpc client: {:?}", endpoint);
|
||||
|
||||
// Create explorer service
|
||||
let service = ExplorerService::new(db_path)?;
|
||||
|
||||
// Initialize the explorer service
|
||||
service.init().await?;
|
||||
|
||||
Ok(Self { rpc_connections: Mutex::new(HashSet::new()), rpc_client, service })
|
||||
}
|
||||
}
|
||||
|
||||
async_daemonize!(realmain);
|
||||
async fn realmain(args: Args, ex: Arc<smol::Executor<'static>>) -> Result<()> {
|
||||
info!(target: "blockchain-explorer", "Initializing DarkFi blockchain explorer node...");
|
||||
let explorer = Explorerd::new(args.db_path, args.endpoint.clone(), ex.clone()).await?;
|
||||
let explorer = Arc::new(explorer);
|
||||
info!(target: "blockchain-explorer", "Node initialized successfully!");
|
||||
|
||||
// JSON-RPC server
|
||||
info!(target: "blockchain-explorer", "Starting JSON-RPC server");
|
||||
// Here we create a task variable so we can manually close the task later.
|
||||
let rpc_task = StoppableTask::new();
|
||||
let explorer_ = explorer.clone();
|
||||
rpc_task.clone().start(
|
||||
listen_and_serve(args.rpc_listen, explorer.clone(), None, ex.clone()),
|
||||
|res| async move {
|
||||
match res {
|
||||
Ok(()) | Err(Error::RpcServerStopped) => explorer_.stop_connections().await,
|
||||
Err(e) => error!(target: "blockchain-explorer", "Failed starting sync JSON-RPC server: {}", e),
|
||||
}
|
||||
},
|
||||
Error::RpcServerStopped,
|
||||
ex.clone(),
|
||||
);
|
||||
|
||||
// Sync blocks
|
||||
info!(target: "blockchain-explorer", "Syncing blocks from darkfid...");
|
||||
if let Err(e) = explorer.sync_blocks(args.reset).await {
|
||||
let error_message = format!("Error syncing blocks: {:?}", e);
|
||||
error!(target: "blockchain-explorer", "{error_message}");
|
||||
return Err(Error::DatabaseError(error_message));
|
||||
}
|
||||
|
||||
// Subscribe blocks
|
||||
info!(target: "blockchain-explorer", "Subscribing to new blocks...");
|
||||
let (subscriber_task, listener_task) =
|
||||
match subscribe_blocks(explorer.clone(), args.endpoint, ex.clone()).await {
|
||||
Ok(pair) => pair,
|
||||
Err(e) => {
|
||||
let error_message = format!("Error setting up blocks subscriber: {:?}", e);
|
||||
error!(target: "blockchain-explorer", "{error_message}");
|
||||
return Err(Error::DatabaseError(error_message));
|
||||
}
|
||||
};
|
||||
|
||||
// Signal handling for graceful termination.
|
||||
let (signals_handler, signals_task) = SignalHandler::new(ex)?;
|
||||
signals_handler.wait_termination(signals_task).await?;
|
||||
info!(target: "blockchain-explorer", "Caught termination signal, cleaning up and exiting...");
|
||||
|
||||
info!(target: "blockchain-explorer", "Stopping JSON-RPC server...");
|
||||
rpc_task.stop().await;
|
||||
|
||||
info!(target: "blockchain-explorer", "Stopping darkfid listener...");
|
||||
listener_task.stop().await;
|
||||
|
||||
info!(target: "blockchain-explorer", "Stopping darkfid subscriber...");
|
||||
subscriber_task.stop().await;
|
||||
|
||||
info!(target: "blockchain-explorer", "Stopping JSON-RPC client...");
|
||||
explorer.rpc_client.stop().await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,139 +0,0 @@
|
||||
/* This file is part of DarkFi (https://dark.fi)
|
||||
*
|
||||
* Copyright (C) 2020-2025 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/>.
|
||||
*/
|
||||
|
||||
use std::{collections::HashSet, time::Instant};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use log::{debug, error, trace};
|
||||
use smol::lock::MutexGuard;
|
||||
use tinyjson::JsonValue;
|
||||
|
||||
use darkfi::{
|
||||
rpc::{
|
||||
jsonrpc::{ErrorCode, JsonError, JsonRequest, JsonResponse, JsonResult},
|
||||
server::RequestHandler,
|
||||
},
|
||||
system::StoppableTaskPtr,
|
||||
Result,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
error::{server_error, RpcError},
|
||||
Explorerd,
|
||||
};
|
||||
|
||||
#[async_trait]
|
||||
impl RequestHandler<()> for Explorerd {
|
||||
async fn handle_request(&self, req: JsonRequest) -> JsonResult {
|
||||
debug!(target: "blockchain-explorer::rpc", "--> {}", req.stringify().unwrap());
|
||||
|
||||
match req.method.as_str() {
|
||||
// =====================
|
||||
// Miscellaneous methods
|
||||
// =====================
|
||||
"ping" => self.pong(req.id, req.params).await,
|
||||
"ping_darkfid" => self.ping_darkfid(req.id, req.params).await,
|
||||
|
||||
// =====================
|
||||
// Blocks methods
|
||||
// =====================
|
||||
"blocks.get_last_n_blocks" => self.blocks_get_last_n_blocks(req.id, req.params).await,
|
||||
"blocks.get_blocks_in_heights_range" => {
|
||||
self.blocks_get_blocks_in_heights_range(req.id, req.params).await
|
||||
}
|
||||
"blocks.get_block_by_hash" => self.blocks_get_block_by_hash(req.id, req.params).await,
|
||||
|
||||
// =====================
|
||||
// Contract methods
|
||||
// =====================
|
||||
"contracts.get_native_contracts" => {
|
||||
self.contracts_get_native_contracts(req.id, req.params).await
|
||||
}
|
||||
"contracts.get_contract_source_code_paths" => {
|
||||
self.contracts_get_contract_source_code_paths(req.id, req.params).await
|
||||
}
|
||||
"contracts.get_contract_source" => {
|
||||
self.contracts_get_contract_source(req.id, req.params).await
|
||||
}
|
||||
|
||||
// =====================
|
||||
// Transactions methods
|
||||
// =====================
|
||||
"transactions.get_transactions_by_header_hash" => {
|
||||
self.transactions_get_transactions_by_header_hash(req.id, req.params).await
|
||||
}
|
||||
"transactions.get_transaction_by_hash" => {
|
||||
self.transactions_get_transaction_by_hash(req.id, req.params).await
|
||||
}
|
||||
|
||||
// =====================
|
||||
// Statistics methods
|
||||
// =====================
|
||||
"statistics.get_basic_statistics" => {
|
||||
self.statistics_get_basic_statistics(req.id, req.params).await
|
||||
}
|
||||
"statistics.get_metric_statistics" => {
|
||||
self.statistics_get_metric_statistics(req.id, req.params).await
|
||||
}
|
||||
|
||||
// TODO: add any other useful methods
|
||||
|
||||
// ==============
|
||||
// Invalid method
|
||||
// ==============
|
||||
_ => JsonError::new(ErrorCode::MethodNotFound, None, req.id).into(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn connections_mut(&self) -> MutexGuard<'_, HashSet<StoppableTaskPtr>> {
|
||||
self.rpc_connections.lock().await
|
||||
}
|
||||
}
|
||||
|
||||
impl Explorerd {
|
||||
// RPCAPI:
|
||||
// Pings configured darkfid daemon for liveness.
|
||||
// Returns `true` on success.
|
||||
//
|
||||
// --> {"jsonrpc": "2.0", "method": "ping_darkfid", "params": [], "id": 1}
|
||||
// <-- {"jsonrpc": "2.0", "result": "true", "id": 1}
|
||||
async fn ping_darkfid(&self, id: u16, _params: JsonValue) -> JsonResult {
|
||||
debug!(target: "blockchain-explorer::rpc::ping_darkfid", "Pinging darkfid daemon...");
|
||||
if let Err(e) = self.darkfid_daemon_request("ping", &JsonValue::Array(vec![])).await {
|
||||
error!(target: "blockchain-explorer::rpc::ping_darkfid", "Failed to ping darkfid daemon: {}", e);
|
||||
return server_error(RpcError::PingFailed, id, None)
|
||||
}
|
||||
JsonResponse::new(JsonValue::Boolean(true), id).into()
|
||||
}
|
||||
|
||||
/// Auxiliary function to execute a request towards the configured darkfid daemon JSON-RPC endpoint.
|
||||
pub async fn darkfid_daemon_request(
|
||||
&self,
|
||||
method: &str,
|
||||
params: &JsonValue,
|
||||
) -> Result<JsonValue> {
|
||||
debug!(target: "blockchain-explorer::rpc::darkfid_daemon_request", "Executing request {} with params: {:?}", method, params);
|
||||
let latency = Instant::now();
|
||||
let req = JsonRequest::new(method, params.clone());
|
||||
let rep = self.rpc_client.request(req).await?;
|
||||
let latency = latency.elapsed();
|
||||
trace!(target: "blockchain-explorer::rpc::darkfid_daemon_request", "Got reply: {:?}", rep);
|
||||
debug!(target: "blockchain-explorer::rpc::darkfid_daemon_request", "Latency: {:?}", latency);
|
||||
Ok(rep)
|
||||
}
|
||||
}
|
||||
@@ -1,536 +0,0 @@
|
||||
/* This file is part of DarkFi (https://dark.fi)
|
||||
*
|
||||
* Copyright (C) 2020-2025 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/>.
|
||||
*/
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use log::{debug, error, info, warn};
|
||||
use tinyjson::JsonValue;
|
||||
use url::Url;
|
||||
|
||||
use darkfi::{
|
||||
blockchain::BlockInfo,
|
||||
rpc::{
|
||||
client::RpcClient,
|
||||
jsonrpc::{
|
||||
ErrorCode::{InternalError, InvalidParams, ParseError},
|
||||
JsonError, JsonRequest, JsonResponse, JsonResult,
|
||||
},
|
||||
},
|
||||
system::{Publisher, StoppableTask, StoppableTaskPtr},
|
||||
util::encoding::base64,
|
||||
Error, Result,
|
||||
};
|
||||
use darkfi_serial::deserialize_async;
|
||||
|
||||
use crate::{error::handle_database_error, Explorerd};
|
||||
|
||||
impl Explorerd {
|
||||
// Queries darkfid for a block with given height.
|
||||
async fn get_darkfid_block_by_height(&self, height: u32) -> Result<BlockInfo> {
|
||||
let params = self
|
||||
.darkfid_daemon_request(
|
||||
"blockchain.get_block",
|
||||
&JsonValue::Array(vec![JsonValue::String(height.to_string())]),
|
||||
)
|
||||
.await?;
|
||||
let param = params.get::<String>().unwrap();
|
||||
let bytes = base64::decode(param).unwrap();
|
||||
let block = deserialize_async(&bytes).await?;
|
||||
Ok(block)
|
||||
}
|
||||
|
||||
/// Synchronizes blocks between the explorer and a Darkfi blockchain node, ensuring
|
||||
/// the database remains consistent by syncing any missing or outdated blocks.
|
||||
///
|
||||
/// If provided `reset` is true, the explorer's blockchain-related and metric sled trees are purged
|
||||
/// and syncing starts from the genesis block. The function also handles reorgs by re-aligning the
|
||||
/// explorer state to the correct height when blocks are outdated. Returns a result indicating
|
||||
/// success or failure.
|
||||
///
|
||||
/// Reorg handling is delegated to the [`Self::process_sync_blocks_reorg`] function, whose
|
||||
/// documentation provides more details on the reorg process during block syncing.
|
||||
pub async fn sync_blocks(&self, reset: bool) -> Result<()> {
|
||||
// Grab last synced block height from the explorer's database.
|
||||
let last_synced_block = self.service.last_block().map_err(|e| {
|
||||
handle_database_error(
|
||||
"rpc_blocks::sync_blocks",
|
||||
"[sync_blocks] Retrieving last synced block failed",
|
||||
e,
|
||||
)
|
||||
})?;
|
||||
|
||||
// Grab the last confirmed block height and hash from the darkfi node
|
||||
let (last_darkfid_height, last_darkfid_hash) = self.get_last_confirmed_block().await?;
|
||||
|
||||
// Initialize the current height to sync from, starting from genesis block if last sync block does not exist
|
||||
let (last_synced_height, last_synced_hash) = last_synced_block
|
||||
.map_or((0, "".to_string()), |(height, header_hash)| (height, header_hash));
|
||||
|
||||
// Declare a mutable variable to track the current sync height while processing blocks
|
||||
let mut current_height = last_synced_height;
|
||||
|
||||
info!(target: "blockchain-explorer::rpc_blocks::sync_blocks", "Requested to sync from block number: {current_height}");
|
||||
info!(target: "blockchain-explorer::rpc_blocks::sync_blocks", "Last confirmed block number reported by darkfid: {last_darkfid_height} - {last_darkfid_hash}");
|
||||
|
||||
// A reorg is detected if the hash of the last synced block differs from the hash of the last confirmed block,
|
||||
// unless the reset flag is set or the current height is 0
|
||||
let reorg_detected = last_synced_hash != last_darkfid_hash && !reset && current_height != 0;
|
||||
|
||||
// If the reset flag is set, reset the explorer state and start syncing from the genesis block height.
|
||||
// Otherwise, handle reorgs if detected, or proceed to the next block if not at the genesis height.
|
||||
if reset {
|
||||
self.service.reset_explorer_state(0)?;
|
||||
current_height = 0;
|
||||
info!(target: "blockchain-explorer::rpc_blocks::sync_blocks", "Successfully reset explorer database based on set reset parameter");
|
||||
} else if reorg_detected {
|
||||
current_height =
|
||||
self.process_sync_blocks_reorg(last_synced_height, last_darkfid_height).await?;
|
||||
// Log only if a reorg occurred
|
||||
if current_height != last_synced_height {
|
||||
info!(target: "blockchain-explorer::rpc_blocks::sync_blocks", "Successfully completed reorg to height: {current_height}");
|
||||
}
|
||||
// Prepare to sync the next block after reorg
|
||||
current_height += 1;
|
||||
} else if current_height != 0 {
|
||||
// Resume syncing from the block after the last synced height
|
||||
current_height += 1;
|
||||
}
|
||||
|
||||
// Sync blocks until the explorer is up to date with the last confirmed block
|
||||
while current_height <= last_darkfid_height {
|
||||
// Retrieve the block from darkfi node by height
|
||||
let block = match self.get_darkfid_block_by_height(current_height).await {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
return Err(handle_database_error(
|
||||
"rpc_blocks::sync_blocks",
|
||||
"[sync_blocks] RPC client request failed",
|
||||
e,
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
// Store the retrieved block in the explorer's database
|
||||
if let Err(e) = self.service.put_block(&block).await {
|
||||
return Err(handle_database_error(
|
||||
"rpc_blocks::sync_blocks",
|
||||
"[sync_blocks] Put block failed",
|
||||
e,
|
||||
))
|
||||
};
|
||||
|
||||
info!(target: "blockchain-explorer::rpc_blocks::sync_blocks", "Synced block {current_height}");
|
||||
|
||||
// Increment the current height to sync the next block
|
||||
current_height += 1;
|
||||
}
|
||||
|
||||
info!(target: "blockchain-explorer::rpc_blocks::sync_blocks", "Completed sync, total number of explorer blocks: {}", self.service.db.blockchain.blocks.len());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Handles blockchain reorganizations (reorgs) during the explorer node's startup synchronization
|
||||
/// with Darkfi nodes, ensuring the explorer provides a consistent and accurate view of the blockchain.
|
||||
///
|
||||
/// A reorg occurs when the blocks stored by the blockchain nodes diverge from those stored by the explorer.
|
||||
/// This function resolves inconsistencies by identifying the point of divergence, searching backward through
|
||||
/// block heights, and comparing block hashes between the explorer database and the blockchain node. Once a
|
||||
/// common block height is found, the explorer is re-aligned to that height.
|
||||
///
|
||||
/// If no common block can be found, the explorer resets to the "genesis height," removing all blocks,
|
||||
/// transactions, and metrics from its database to resynchronize with the canonical chain from the nodes.
|
||||
///
|
||||
/// Returns the last height at which the explorer's state was successfully re-aligned with the blockchain.
|
||||
async fn process_sync_blocks_reorg(
|
||||
&self,
|
||||
last_synced_height: u32,
|
||||
last_darkfid_height: u32,
|
||||
) -> Result<u32> {
|
||||
// Log reorg detection in the case that explorer height is greater or equal to height of darkfi node
|
||||
if last_synced_height >= last_darkfid_height {
|
||||
info!(target: "blockchain-explorer::rpc_blocks::process_sync_blocks_reorg",
|
||||
"Reorg detected with heights: explorer.{last_synced_height} >= darkfid.{last_darkfid_height}");
|
||||
}
|
||||
|
||||
// Declare a mutable variable to track the current height while searching for a common block
|
||||
let mut cur_height = last_synced_height;
|
||||
// Search for an explorer block that matches a darkfi node block
|
||||
while cur_height > 0 {
|
||||
let synced_block = self.service.get_block_by_height(cur_height)?;
|
||||
debug!(target: "blockchain-explorer::rpc_blocks::process_sync_blocks_reorg", "Searching for common block: {}", cur_height);
|
||||
|
||||
// Check if we found a synced block for current height being searched
|
||||
if let Some(synced_block) = synced_block {
|
||||
// Fetch the block from darkfi node to check for a match
|
||||
match self.get_darkfid_block_by_height(cur_height).await {
|
||||
Ok(darkfid_block) => {
|
||||
// If hashes match, we've found the point of divergence
|
||||
if synced_block.header_hash == darkfid_block.hash().to_string() {
|
||||
// If hashes match but the cur_height differs from the last synced height, reset the explorer state
|
||||
if cur_height != last_synced_height {
|
||||
self.service.reset_explorer_state(cur_height)?;
|
||||
debug!(target: "blockchain-explorer::rpc_blocks::process_sync_blocks_reorg", "Successfully completed reorg to height: {cur_height}");
|
||||
}
|
||||
break;
|
||||
} else {
|
||||
// Log reorg detection with height and header hash mismatch details
|
||||
if cur_height == last_synced_height {
|
||||
info!(
|
||||
target: "blockchain-explorer::rpc_blocks::process_sync_blocks_reorg",
|
||||
"Reorg detected at height {}: explorer.{} != darkfid.{}",
|
||||
cur_height,
|
||||
synced_block.header_hash,
|
||||
darkfid_block.hash().to_string()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Continue searching for blocks that do not exist on darkfi nodes
|
||||
Err(Error::JsonRpcError((-32121, _))) => (),
|
||||
Err(e) => {
|
||||
return Err(handle_database_error(
|
||||
"rpc_blocks::process_sync_blocks_reorg",
|
||||
"[process_sync_blocks_reorg] RPC client request failed",
|
||||
e,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Move to previous block to search for a match
|
||||
cur_height = cur_height.saturating_sub(1);
|
||||
}
|
||||
|
||||
// Check if genesis block reorg is needed
|
||||
if cur_height == 0 {
|
||||
self.service.reset_explorer_state(0)?;
|
||||
}
|
||||
|
||||
// Return the last height we reorged to
|
||||
Ok(cur_height)
|
||||
}
|
||||
|
||||
// RPCAPI:
|
||||
// Queries the database to retrieve last N blocks.
|
||||
// Returns an array of readable blocks upon success.
|
||||
//
|
||||
// **Params:**
|
||||
// * `array[0]`: `u16` Number of blocks to retrieve (as string)
|
||||
//
|
||||
// **Returns:**
|
||||
// * Array of `BlockRecord` encoded into a JSON.
|
||||
//
|
||||
// --> {"jsonrpc": "2.0", "method": "blocks.get_last_n_blocks", "params": ["10"], "id": 1}
|
||||
// <-- {"jsonrpc": "2.0", "result": {...}, "id": 1}
|
||||
pub async fn blocks_get_last_n_blocks(&self, id: u16, params: JsonValue) -> JsonResult {
|
||||
let params = params.get::<Vec<JsonValue>>().unwrap();
|
||||
if params.len() != 1 || !params[0].is_string() {
|
||||
return JsonError::new(InvalidParams, None, id).into()
|
||||
}
|
||||
|
||||
// Extract the number of last blocks to retrieve from parameters
|
||||
let n = match params[0].get::<String>().unwrap().parse::<usize>() {
|
||||
Ok(v) => v,
|
||||
Err(_) => return JsonError::new(ParseError, None, id).into(),
|
||||
};
|
||||
|
||||
// Fetch the blocks and handle potential errors
|
||||
let blocks_result = match self.service.get_last_n(n) {
|
||||
Ok(blocks) => blocks,
|
||||
Err(e) => {
|
||||
error!(target: "blockchain-explorer::rpc_blocks::blocks_get_last_n_blocks", "Failed fetching blocks: {}", e);
|
||||
return JsonError::new(InternalError, None, id).into();
|
||||
}
|
||||
};
|
||||
|
||||
// Transform blocks to json and return result
|
||||
if blocks_result.is_empty() {
|
||||
JsonResponse::new(JsonValue::Array(vec![]), id).into()
|
||||
} else {
|
||||
let json_blocks: Vec<JsonValue> =
|
||||
blocks_result.into_iter().map(|block| block.to_json_array()).collect();
|
||||
JsonResponse::new(JsonValue::Array(json_blocks), id).into()
|
||||
}
|
||||
}
|
||||
|
||||
// RPCAPI:
|
||||
// Queries the database to retrieve blocks in provided heights range.
|
||||
// Returns an array of readable blocks upon success.
|
||||
//
|
||||
// **Params:**
|
||||
// * `array[0]`: `u32` Starting height (as string)
|
||||
// * `array[1]`: `u32` Ending height range (as string)
|
||||
//
|
||||
// **Returns:**
|
||||
// * Array of `BlockRecord` encoded into a JSON.
|
||||
//
|
||||
// --> {"jsonrpc": "2.0", "method": "blocks.get_blocks_in_heights_range", "params": ["10", "15"], "id": 1}
|
||||
// <-- {"jsonrpc": "2.0", "result": {...}, "id": 1}
|
||||
pub async fn blocks_get_blocks_in_heights_range(
|
||||
&self,
|
||||
id: u16,
|
||||
params: JsonValue,
|
||||
) -> JsonResult {
|
||||
let params = params.get::<Vec<JsonValue>>().unwrap();
|
||||
if params.len() != 2 || !params[0].is_string() || !params[1].is_string() {
|
||||
return JsonError::new(InvalidParams, None, id).into()
|
||||
}
|
||||
|
||||
let start = match params[0].get::<String>().unwrap().parse::<u32>() {
|
||||
Ok(v) => v,
|
||||
Err(_) => return JsonError::new(ParseError, None, id).into(),
|
||||
};
|
||||
|
||||
let end = match params[1].get::<String>().unwrap().parse::<u32>() {
|
||||
Ok(v) => v,
|
||||
Err(_) => return JsonError::new(ParseError, None, id).into(),
|
||||
};
|
||||
|
||||
if start > end {
|
||||
return JsonError::new(ParseError, None, id).into()
|
||||
}
|
||||
|
||||
// Fetch the blocks and handle potential errors
|
||||
let blocks_result = match self.service.get_by_range(start, end) {
|
||||
Ok(blocks) => blocks,
|
||||
Err(e) => {
|
||||
error!(target: "blockchain-explorer::rpc_blocks::blocks_get_blocks_in_height_range", "Failed fetching blocks: {}", e);
|
||||
return JsonError::new(InternalError, None, id).into();
|
||||
}
|
||||
};
|
||||
|
||||
// Transform blocks to json and return result
|
||||
if blocks_result.is_empty() {
|
||||
JsonResponse::new(JsonValue::Array(vec![]), id).into()
|
||||
} else {
|
||||
let json_blocks: Vec<JsonValue> =
|
||||
blocks_result.into_iter().map(|block| block.to_json_array()).collect();
|
||||
JsonResponse::new(JsonValue::Array(json_blocks), id).into()
|
||||
}
|
||||
}
|
||||
|
||||
// RPCAPI:
|
||||
// Queries the database to retrieve the block corresponding to the provided hash.
|
||||
// Returns the readable block upon success.
|
||||
//
|
||||
// **Params:**
|
||||
// * `array[0]`: `String` Block header hash
|
||||
//
|
||||
// **Returns:**
|
||||
// * `BlockRecord` encoded into a JSON.
|
||||
//
|
||||
// --> {"jsonrpc": "2.0", "method": "blocks.get_block_by_hash", "params": ["5cc...2f9"], "id": 1}
|
||||
// <-- {"jsonrpc": "2.0", "result": {...}, "id": 1}
|
||||
pub async fn blocks_get_block_by_hash(&self, id: u16, params: JsonValue) -> JsonResult {
|
||||
let params = params.get::<Vec<JsonValue>>().unwrap();
|
||||
if params.len() != 1 || !params[0].is_string() {
|
||||
return JsonError::new(InvalidParams, None, id).into()
|
||||
}
|
||||
|
||||
// Extract header hash from params, returning error if not provided
|
||||
let header_hash = match params[0].get::<String>() {
|
||||
Some(hash) => hash,
|
||||
None => return JsonError::new(InvalidParams, None, id).into(),
|
||||
};
|
||||
|
||||
// Fetch and transform block to json, handling any errors and returning the result
|
||||
match self.service.get_block_by_hash(header_hash) {
|
||||
Ok(Some(block)) => JsonResponse::new(block.to_json_array(), id).into(),
|
||||
Ok(None) => JsonResponse::new(JsonValue::Array(vec![]), id).into(),
|
||||
Err(e) => {
|
||||
error!(target: "blockchain-explorer::rpc_blocks", "Failed fetching block: {:?}", e);
|
||||
JsonError::new(InternalError, None, id).into()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Queries darkfid for last confirmed block.
|
||||
async fn get_last_confirmed_block(&self) -> Result<(u32, String)> {
|
||||
let rep = self
|
||||
.darkfid_daemon_request("blockchain.last_confirmed_block", &JsonValue::Array(vec![]))
|
||||
.await?;
|
||||
let params = rep.get::<Vec<JsonValue>>().unwrap();
|
||||
let height = *params[0].get::<f64>().unwrap() as u32;
|
||||
let hash = params[1].get::<String>().unwrap().clone();
|
||||
|
||||
Ok((height, hash))
|
||||
}
|
||||
}
|
||||
|
||||
/// Subscribes to darkfid's JSON-RPC notification endpoint that serves
|
||||
/// new confirmed blocks. Upon receiving them, store them to the database.
|
||||
pub async fn subscribe_blocks(
|
||||
explorer: Arc<Explorerd>,
|
||||
endpoint: Url,
|
||||
ex: Arc<smol::Executor<'static>>,
|
||||
) -> Result<(StoppableTaskPtr, StoppableTaskPtr)> {
|
||||
// Grab last confirmed block
|
||||
let (last_darkfid_height, last_darkfid_hash) = explorer.get_last_confirmed_block().await?;
|
||||
|
||||
// Grab last synced block
|
||||
let (mut height, hash) = match explorer.service.last_block() {
|
||||
Ok(Some((height, hash))) => (height, hash),
|
||||
Ok(None) => (0, "".to_string()),
|
||||
Err(e) => {
|
||||
return Err(Error::DatabaseError(format!(
|
||||
"[subscribe_blocks] Retrieving last synced block failed: {e:?}"
|
||||
)))
|
||||
}
|
||||
};
|
||||
|
||||
// Evaluates whether there is a mismatch between the last confirmed block and the last synced block
|
||||
let blocks_mismatch = (last_darkfid_height != height || last_darkfid_hash != hash) &&
|
||||
last_darkfid_height != 0 &&
|
||||
height != 0;
|
||||
|
||||
// Check if there is a mismatch, throwing an error to prevent operating in a potentially inconsistent state
|
||||
if blocks_mismatch {
|
||||
warn!(target: "blockchain-explorer::rpc_blocks::subscribe_blocks",
|
||||
"Warning: Last synced block is not the last confirmed block: \
|
||||
last_darkfid_height={last_darkfid_height}, last_synced_height={height}, last_darkfid_hash={last_darkfid_hash}, last_synced_hash={hash}");
|
||||
warn!(target: "blockchain-explorer::rpc_blocks::subscribe_blocks", "You should first fully sync the blockchain, and then subscribe");
|
||||
return Err(Error::DatabaseError(
|
||||
"[subscribe_blocks] Blockchain not fully synced".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
info!(target: "blockchain-explorer::rpc_blocks::subscribe_blocks", "Subscribing to receive notifications of incoming blocks");
|
||||
let publisher = Publisher::new();
|
||||
let subscription = publisher.clone().subscribe().await;
|
||||
let _ex = ex.clone();
|
||||
let subscriber_task = StoppableTask::new();
|
||||
subscriber_task.clone().start(
|
||||
// Weird hack to prevent lifetimes hell
|
||||
async move {
|
||||
let ex = _ex.clone();
|
||||
let rpc_client = RpcClient::new(endpoint, ex).await?;
|
||||
let req = JsonRequest::new("blockchain.subscribe_blocks", JsonValue::Array(vec![]));
|
||||
rpc_client.subscribe(req, publisher).await
|
||||
},
|
||||
|res| async move {
|
||||
match res {
|
||||
Ok(()) => { /* Do nothing */ }
|
||||
Err(e) => error!(target: "blockchain-explorer::rpc_blocks::subscribe_blocks", "[subscribe_blocks] JSON-RPC server error: {e:?}"),
|
||||
}
|
||||
},
|
||||
Error::RpcServerStopped,
|
||||
ex.clone(),
|
||||
);
|
||||
info!(target: "blockchain-explorer::rpc_blocks::subscribe_blocks", "Detached subscription to background");
|
||||
info!(target: "blockchain-explorer::rpc_blocks::subscribe_blocks", "All is good. Waiting for block notifications...");
|
||||
|
||||
let listener_task = StoppableTask::new();
|
||||
listener_task.clone().start(
|
||||
// Weird hack to prevent lifetimes hell
|
||||
async move {
|
||||
loop {
|
||||
match subscription.receive().await {
|
||||
JsonResult::Notification(n) => {
|
||||
debug!(target: "blockchain-explorer::rpc_blocks::subscribe_blocks", "Got Block notification from darkfid subscription");
|
||||
if n.method != "blockchain.subscribe_blocks" {
|
||||
return Err(Error::UnexpectedJsonRpc(format!(
|
||||
"Got foreign notification from darkfid: {}",
|
||||
n.method
|
||||
)))
|
||||
}
|
||||
|
||||
// Verify parameters
|
||||
if !n.params.is_array() {
|
||||
return Err(Error::UnexpectedJsonRpc(
|
||||
"Received notification params are not an array".to_string(),
|
||||
))
|
||||
}
|
||||
let params = n.params.get::<Vec<JsonValue>>().unwrap();
|
||||
if params.is_empty() {
|
||||
return Err(Error::UnexpectedJsonRpc(
|
||||
"Notification parameters are empty".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
for param in params {
|
||||
let param = param.get::<String>().unwrap();
|
||||
let bytes = base64::decode(param).unwrap();
|
||||
|
||||
let darkfid_block: BlockInfo = match deserialize_async(&bytes).await {
|
||||
Ok(b) => b,
|
||||
Err(e) => {
|
||||
return Err(Error::UnexpectedJsonRpc(format!(
|
||||
"[subscribe_blocks] Deserializing block failed: {e:?}"
|
||||
)))
|
||||
},
|
||||
};
|
||||
info!(target: "blockchain-explorer::rpc_blocks::subscribe_blocks", "=======================================");
|
||||
info!(target: "blockchain-explorer::rpc_blocks::subscribe_blocks", "Block Notification: {}", darkfid_block.hash().to_string());
|
||||
info!(target: "blockchain-explorer::rpc_blocks::subscribe_blocks", "=======================================");
|
||||
|
||||
// Store darkfi node block height for later use
|
||||
let darkfid_block_height = darkfid_block.header.height;
|
||||
|
||||
// Check if we need to perform a reorg due to mismatch in block heights
|
||||
if darkfid_block_height <= height {
|
||||
info!(target: "blockchain-explorer::rpc_blocks::subscribe_blocks",
|
||||
"Reorg detected with heights: darkfid.{darkfid_block_height} <= explorer.{height}");
|
||||
|
||||
// Calculate the reset height
|
||||
let reset_height = darkfid_block_height.saturating_sub(1);
|
||||
|
||||
// Execute the reorg by resetting the explorer state to reset height
|
||||
explorer.service.reset_explorer_state(reset_height)?;
|
||||
info!(target: "blockchain-explorer::rpc_blocks::subscribe_blocks", "Successfully completed reorg to height: {reset_height}");
|
||||
}
|
||||
|
||||
if let Err(e) = explorer.service.put_block(&darkfid_block).await {
|
||||
return Err(Error::DatabaseError(format!(
|
||||
"[subscribe_blocks] Put block failed: {e:?}"
|
||||
)))
|
||||
}
|
||||
|
||||
info!(target: "blockchain-explorer::rpc_blocks::subscribe_blocks", "Successfully stored new block at height: {}", darkfid_block.header.height );
|
||||
|
||||
// Process the next block
|
||||
height = darkfid_block.header.height;
|
||||
}
|
||||
}
|
||||
|
||||
JsonResult::Error(e) => {
|
||||
// Some error happened in the transmission
|
||||
return Err(Error::UnexpectedJsonRpc(format!("Got error from JSON-RPC: {e:?}")))
|
||||
}
|
||||
|
||||
x => {
|
||||
// And this is weird
|
||||
return Err(Error::UnexpectedJsonRpc(format!(
|
||||
"Got unexpected data from JSON-RPC: {x:?}"
|
||||
)))
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
|res| async move {
|
||||
match res {
|
||||
Ok(()) => { /* Do nothing */ }
|
||||
Err(e) => error!(target: "blockchain-explorer::rpc_blocks::subscribe_blocks", "[subscribe_blocks] JSON-RPC server error: {e:?}"),
|
||||
}
|
||||
},
|
||||
Error::RpcServerStopped,
|
||||
ex,
|
||||
);
|
||||
|
||||
Ok((subscriber_task, listener_task))
|
||||
}
|
||||
@@ -1,168 +0,0 @@
|
||||
/* This file is part of DarkFi (https://dark.fi)
|
||||
*
|
||||
* Copyright (C) 2020-2025 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/>.
|
||||
*/
|
||||
|
||||
use std::str::FromStr;
|
||||
|
||||
use log::error;
|
||||
use tinyjson::JsonValue;
|
||||
|
||||
use darkfi::rpc::jsonrpc::{
|
||||
ErrorCode::{InternalError, InvalidParams},
|
||||
JsonError, JsonResponse, JsonResult,
|
||||
};
|
||||
use darkfi_sdk::crypto::ContractId;
|
||||
|
||||
use crate::Explorerd;
|
||||
|
||||
impl Explorerd {
|
||||
// RPCAPI:
|
||||
// Retrieves the native contracts deployed in the DarkFi network.
|
||||
// Returns a JSON array containing Contract IDs along with their associated metadata upon success.
|
||||
//
|
||||
// **Params:**
|
||||
// * `None`
|
||||
//
|
||||
// **Returns:**
|
||||
// * Array of `ContractRecord`s encoded into a JSON.
|
||||
//
|
||||
// --> {"jsonrpc": "2.0", "method": "contracts.get_native_contracts", "params": ["5cc...2f9"], "id": 1}
|
||||
// <-- {"jsonrpc": "2.0", "result": ["BZHKGQ26bzmBithTQYTJtjo2QdCqpkR9tjSBopT4yf4o", "Money Contract", "The money contract..."], "id": 1}
|
||||
pub async fn contracts_get_native_contracts(&self, id: u16, params: JsonValue) -> JsonResult {
|
||||
// Ensure that the parameters are empty
|
||||
let params = params.get::<Vec<JsonValue>>().unwrap();
|
||||
if !params.is_empty() {
|
||||
return JsonError::new(InvalidParams, None, id).into()
|
||||
}
|
||||
|
||||
// Retrieve native contracts and handle potential errors
|
||||
let contract_records = match self.service.get_native_contracts() {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
error!(target: "explorerd::rpc_contracts::contracts_get_native_contracts", "Failed fetching native contracts: {}", e);
|
||||
return JsonError::new(InternalError, None, id).into()
|
||||
}
|
||||
};
|
||||
|
||||
// Transform contract records into a JSON array and return the result
|
||||
if contract_records.is_empty() {
|
||||
JsonResponse::new(JsonValue::Array(vec![]), id).into()
|
||||
} else {
|
||||
let json_blocks: Vec<JsonValue> = contract_records
|
||||
.into_iter()
|
||||
.map(|contract_record| contract_record.to_json_array())
|
||||
.collect();
|
||||
JsonResponse::new(JsonValue::Array(json_blocks), id).into()
|
||||
}
|
||||
}
|
||||
|
||||
// RPCAPI:
|
||||
// Retrieves the source code paths for the contract associated with the specified Contract ID.
|
||||
// Returns a JSON array containing the source code paths upon success.
|
||||
//
|
||||
// **Params:**
|
||||
// * `array[0]`: `String` Contract ID
|
||||
//
|
||||
// **Returns:**
|
||||
// * `JsonArray` containing source code paths for the specified Contract ID.
|
||||
//
|
||||
// Example Call:
|
||||
// --> {"jsonrpc": "2.0", "method": "contracts.get_contract_source_code_paths", "params": ["BZHKGQ26bzmBithTQYTJtjo2QdCqpkR9tjSBopT4yf4o"], "id": 1}
|
||||
// <-- {"jsonrpc": "2.0", "result": ["path/to/source1.rs", "path/to/source2.rs"], "id": 1}
|
||||
pub async fn contracts_get_contract_source_code_paths(
|
||||
&self,
|
||||
id: u16,
|
||||
params: JsonValue,
|
||||
) -> JsonResult {
|
||||
// Validate that a single required parameter is provided and is of type String
|
||||
let params = params.get::<Vec<JsonValue>>().unwrap();
|
||||
if params.len() != 1 || !params[0].is_string() {
|
||||
return JsonError::new(InvalidParams, None, id).into()
|
||||
}
|
||||
|
||||
// Validate the provided contract ID and convert it into a ContractId object
|
||||
let contact_id_str = params[0].get::<String>().unwrap();
|
||||
let contract_id = match ContractId::from_str(contact_id_str) {
|
||||
Ok(contract_id) => contract_id,
|
||||
Err(e) => return JsonError::new(InternalError, Some(e.to_string()), id).into(),
|
||||
};
|
||||
|
||||
// Retrieve source code paths for the contract, transform them into a JsonResponse, and return the result
|
||||
match self.service.get_contract_source_paths(&contract_id) {
|
||||
Ok(paths) => {
|
||||
let transformed_paths =
|
||||
paths.iter().map(|path| JsonValue::String(path.clone())).collect();
|
||||
JsonResponse::new(JsonValue::Array(transformed_paths), id).into()
|
||||
}
|
||||
Err(e) => {
|
||||
error!(
|
||||
target: "explorerd::rpc_contracts::contracts_get_contract_source_code_paths",
|
||||
"Failed fetching contract source code paths: {e:?}");
|
||||
JsonError::new(InternalError, None, id).into()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RPCAPI:
|
||||
// Retrieves contract source code content using the provided Contract ID and source path.
|
||||
// Returns the source code content as a JSON string upon success.
|
||||
//
|
||||
// **Params:**
|
||||
// * `array[0]`: `String` Contract ID
|
||||
// * `array[1]`: `String` Source path
|
||||
//
|
||||
// **Returns:**
|
||||
// * `String` containing the content of the contract source file.
|
||||
//
|
||||
// Example Call:
|
||||
// --> {"jsonrpc": "2.0", "method": "contracts.get_contract_source", "params": ["BZHKGQ26bzmBithTQYTJtjo2QdCqpkR9tjSBopT4yf4o", "client/lib.rs"], "id": 1}
|
||||
// <-- {"jsonrpc": "2.0", "result": "/* This file is ...", "id": 1}
|
||||
pub async fn contracts_get_contract_source(&self, id: u16, params: JsonValue) -> JsonResult {
|
||||
// Validate that the required parameters are provided
|
||||
let params = params.get::<Vec<JsonValue>>().unwrap();
|
||||
if params.len() != 2 || !params[0].is_string() || !params[1].is_string() {
|
||||
return JsonError::new(InvalidParams, None, id).into()
|
||||
}
|
||||
|
||||
// Validate and extract the provided Contract ID
|
||||
let contact_id_str = params[0].get::<String>().unwrap();
|
||||
let contract_id = match ContractId::from_str(contact_id_str) {
|
||||
Ok(contract_id) => contract_id,
|
||||
Err(e) => return JsonError::new(InternalError, Some(e.to_string()), id).into(),
|
||||
};
|
||||
|
||||
// Extract the provided source path
|
||||
let source_path = params[1].get::<String>().unwrap();
|
||||
|
||||
// Retrieve the contract source code, transform it into a JsonResponse, and return the result
|
||||
match self.service.get_contract_source_content(&contract_id, source_path) {
|
||||
Ok(Some(source_file)) => JsonResponse::new(JsonValue::String(source_file), id).into(),
|
||||
Ok(None) => {
|
||||
let empty_value =
|
||||
JsonValue::from(std::collections::HashMap::<String, JsonValue>::new());
|
||||
JsonResponse::new(empty_value, id).into()
|
||||
}
|
||||
Err(e) => {
|
||||
error!(
|
||||
target: "explorerd::rpc_contracts::contracts_get_contract_source",
|
||||
"Failed fetching contract source code: {}", e
|
||||
);
|
||||
JsonError::new(InternalError, None, id).into()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
/* This file is part of DarkFi (https://dark.fi)
|
||||
*
|
||||
* Copyright (C) 2020-2025 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/>.
|
||||
*/
|
||||
|
||||
use std::vec::Vec;
|
||||
|
||||
use log::error;
|
||||
use tinyjson::JsonValue;
|
||||
|
||||
use darkfi::rpc::jsonrpc::{
|
||||
ErrorCode::{InternalError, InvalidParams},
|
||||
JsonError, JsonResponse, JsonResult,
|
||||
};
|
||||
|
||||
use crate::Explorerd;
|
||||
|
||||
impl Explorerd {
|
||||
// RPCAPI:
|
||||
// Queries the database to retrieve current basic statistics.
|
||||
// Returns the readable transaction upon success.
|
||||
//
|
||||
// **Params:**
|
||||
// * `None`
|
||||
//
|
||||
// **Returns:**
|
||||
// * `BaseStatistics` encoded into a JSON.
|
||||
//
|
||||
// --> {"jsonrpc": "2.0", "method": "statistics.get_basic_statistics", "params": [], "id": 1}
|
||||
// <-- {"jsonrpc": "2.0", "result": {...}, "id": 1}
|
||||
pub async fn statistics_get_basic_statistics(&self, id: u16, params: JsonValue) -> JsonResult {
|
||||
// Validate to ensure parameters are empty
|
||||
let params = params.get::<Vec<JsonValue>>().unwrap();
|
||||
if !params.is_empty() {
|
||||
return JsonError::new(InvalidParams, None, id).into()
|
||||
}
|
||||
|
||||
// Fetch `BaseStatistics`, transform to `JsonResult`, and return results
|
||||
match self.service.get_base_statistics() {
|
||||
Ok(Some(statistics)) => JsonResponse::new(statistics.to_json_array(), id).into(),
|
||||
Ok(None) => JsonResponse::new(JsonValue::Array(vec![]), id).into(),
|
||||
Err(e) => {
|
||||
error!(
|
||||
target: "blockchain-explorer::rpc_statistics::statistics_get_basic_statistics",
|
||||
"Failed fetching basic statistics: {}", e
|
||||
);
|
||||
JsonError::new(InternalError, None, id).into()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RPCAPI:
|
||||
// Queries the database to retrieve all metrics statistics.
|
||||
// Returns a collection of metric statistics upon success.
|
||||
//
|
||||
// **Params:**
|
||||
// * `None`
|
||||
//
|
||||
// **Returns:**
|
||||
// * `MetricsStatistics` array encoded into a JSON.
|
||||
//
|
||||
// --> {"jsonrpc": "2.0", "method": "statistics.get_metric_statistics", "params": [], "id": 1}
|
||||
// <-- {"jsonrpc": "2.0", "result": {...}, "id": 1}
|
||||
pub async fn statistics_get_metric_statistics(&self, id: u16, params: JsonValue) -> JsonResult {
|
||||
// Validate to ensure parameters are empty
|
||||
let params = params.get::<Vec<JsonValue>>().unwrap();
|
||||
if !params.is_empty() {
|
||||
return JsonError::new(InvalidParams, None, id).into()
|
||||
}
|
||||
// Fetch metric statistics and return results
|
||||
let metrics = match self.service.get_metrics_statistics().await {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
error!(target: "blockchain-explorer::rpc_statistics::statistics_get_metric_statistics", "Failed fetching metric statistics: {}", e);
|
||||
return JsonError::new(InternalError, None, id).into()
|
||||
}
|
||||
};
|
||||
|
||||
// Transform statistics to JsonResponse and return result
|
||||
let metrics_json: Vec<JsonValue> = metrics.iter().map(|m| m.to_json_array()).collect();
|
||||
JsonResponse::new(JsonValue::Array(metrics_json), id).into()
|
||||
}
|
||||
|
||||
// RPCAPI:
|
||||
// Queries the database to retrieve latest metric statistics.
|
||||
// Returns the readable metric statistics upon success.
|
||||
//
|
||||
// **Params:**
|
||||
// * `None`
|
||||
//
|
||||
// **Returns:**
|
||||
// * `MetricsStatistics` encoded into a JSON.
|
||||
//
|
||||
// --> {"jsonrpc": "2.0", "method": "statistics.get_latest_metric_statistics", "params": [], "id": 1}
|
||||
// <-- {"jsonrpc": "2.0", "result": {...}, "id": 1}
|
||||
pub async fn statistics_get_latest_metric_statistics(
|
||||
&self,
|
||||
id: u16,
|
||||
params: JsonValue,
|
||||
) -> JsonResult {
|
||||
// Validate to ensure parameters are empty
|
||||
let params = params.get::<Vec<JsonValue>>().unwrap();
|
||||
if !params.is_empty() {
|
||||
return JsonError::new(InvalidParams, None, id).into()
|
||||
}
|
||||
// Fetch metric statistics and return results
|
||||
let metrics = match self.service.get_latest_metrics_statistics().await {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
error!(target: "blockchain-explorer::rpc_statistics::statistics_get_latest_metric_statistics", "Failed fetching metric statistics: {}", e);
|
||||
return JsonError::new(InternalError, None, id).into()
|
||||
}
|
||||
};
|
||||
|
||||
// Transform statistics to JsonResponse and return result
|
||||
JsonResponse::new(metrics.to_json_array(), id).into()
|
||||
}
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
/* This file is part of DarkFi (https://dark.fi)
|
||||
*
|
||||
* Copyright (C) 2020-2025 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/>.
|
||||
*/
|
||||
|
||||
use log::error;
|
||||
use tinyjson::JsonValue;
|
||||
|
||||
use darkfi::rpc::jsonrpc::{
|
||||
ErrorCode::{InternalError, InvalidParams},
|
||||
JsonError, JsonResponse, JsonResult,
|
||||
};
|
||||
use darkfi_sdk::tx::TransactionHash;
|
||||
|
||||
use crate::Explorerd;
|
||||
|
||||
impl Explorerd {
|
||||
// RPCAPI:
|
||||
// Queries the database to retrieve the transactions corresponding to the provided block header hash.
|
||||
// Returns the readable transactions upon success.
|
||||
//
|
||||
// **Params:**
|
||||
// * `array[0]`: `String` Block header hash
|
||||
//
|
||||
// **Returns:**
|
||||
// * Array of `TransactionRecord` encoded into a JSON.
|
||||
//
|
||||
// --> {"jsonrpc": "2.0", "method": "transactions.get_transactions_by_header_hash", "params": ["5cc...2f9"], "id": 1}
|
||||
// <-- {"jsonrpc": "2.0", "result": {...}, "id": 1}
|
||||
pub async fn transactions_get_transactions_by_header_hash(
|
||||
&self,
|
||||
id: u16,
|
||||
params: JsonValue,
|
||||
) -> JsonResult {
|
||||
let params = params.get::<Vec<JsonValue>>().unwrap();
|
||||
if params.len() != 1 || !params[0].is_string() {
|
||||
return JsonError::new(InvalidParams, None, id).into()
|
||||
}
|
||||
|
||||
let header_hash = params[0].get::<String>().unwrap();
|
||||
let transactions = match self.service.get_transactions_by_header_hash(header_hash) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
error!(target: "blockchain-explorer::rpc_transactions::transactions_get_transaction_by_header_hash", "Failed fetching block transactions: {}", e);
|
||||
return JsonError::new(InternalError, None, id).into()
|
||||
}
|
||||
};
|
||||
|
||||
let mut ret = vec![];
|
||||
for transaction in transactions {
|
||||
ret.push(transaction.to_json_array());
|
||||
}
|
||||
JsonResponse::new(JsonValue::Array(ret), id).into()
|
||||
}
|
||||
|
||||
// RPCAPI:
|
||||
// Queries the database to retrieve the transaction corresponding to the provided hash.
|
||||
// Returns the readable transaction upon success.
|
||||
//
|
||||
// **Params:**
|
||||
// * `array[0]`: `String` Transaction hash
|
||||
//
|
||||
// **Returns:**
|
||||
// * `TransactionRecord` encoded into a JSON.
|
||||
//
|
||||
// --> {"jsonrpc": "2.0", "method": "transactions.get_transaction_by_hash", "params": ["7e7...b4d"], "id": 1}
|
||||
// <-- {"jsonrpc": "2.0", "result": {...}, "id": 1}
|
||||
pub async fn transactions_get_transaction_by_hash(
|
||||
&self,
|
||||
id: u16,
|
||||
params: JsonValue,
|
||||
) -> JsonResult {
|
||||
let params = params.get::<Vec<JsonValue>>().unwrap();
|
||||
if params.len() != 1 || !params[0].is_string() {
|
||||
return JsonError::new(InvalidParams, None, id).into()
|
||||
}
|
||||
|
||||
// Validate provided hash and store it for later use
|
||||
let tx_hash_str = params[0].get::<String>().unwrap();
|
||||
let tx_hash = match tx_hash_str.parse::<TransactionHash>() {
|
||||
Ok(hash) => hash,
|
||||
Err(e) => return JsonError::new(InternalError, Some(e.to_string()), id).into(),
|
||||
};
|
||||
|
||||
// Retrieve transaction by hash and return result
|
||||
match self.service.get_transaction_by_hash(&tx_hash) {
|
||||
Ok(Some(transaction)) => JsonResponse::new(transaction.to_json_array(), id).into(),
|
||||
Ok(None) => JsonResponse::new(JsonValue::Array(vec![]), id).into(),
|
||||
Err(e) => {
|
||||
error!(target: "blockchain-explorer::rpc_transactions::transactions_get_transaction_by_hash", "Failed fetching transaction: {}", e);
|
||||
JsonError::new(InternalError, None, id).into()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,143 +0,0 @@
|
||||
/* This file is part of DarkFi (https://dark.fi)
|
||||
*
|
||||
* Copyright (C) 2020-2025 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/>.
|
||||
*/
|
||||
|
||||
use tinyjson::JsonValue;
|
||||
|
||||
use darkfi::{Error, Result};
|
||||
use darkfi_sdk::blockchain::block_epoch;
|
||||
|
||||
use crate::{metrics_store::GasMetrics, ExplorerService};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
/// Structure representing basic statistic extracted from the database.
|
||||
pub struct BaseStatistics {
|
||||
/// Current blockchain height
|
||||
pub height: u32,
|
||||
/// Current blockchain epoch (based on current height)
|
||||
pub epoch: u8,
|
||||
/// Blockchains' last block hash
|
||||
pub last_block: String,
|
||||
/// Blockchain total blocks
|
||||
pub total_blocks: usize,
|
||||
/// Blockchain total transactions
|
||||
pub total_txs: usize,
|
||||
}
|
||||
|
||||
impl BaseStatistics {
|
||||
/// Auxiliary function to convert `BaseStatistics` into a `JsonValue` array.
|
||||
pub fn to_json_array(&self) -> JsonValue {
|
||||
JsonValue::Array(vec![
|
||||
JsonValue::Number(self.height as f64),
|
||||
JsonValue::Number(self.epoch as f64),
|
||||
JsonValue::String(self.last_block.clone()),
|
||||
JsonValue::Number(self.total_blocks as f64),
|
||||
JsonValue::Number(self.total_txs as f64),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
/// Structure representing metrics extracted from the database.
|
||||
#[derive(Default)]
|
||||
pub struct MetricStatistics {
|
||||
/// Metrics used to store explorer statistics
|
||||
pub metrics: GasMetrics,
|
||||
}
|
||||
|
||||
impl MetricStatistics {
|
||||
pub fn new(metrics: GasMetrics) -> Self {
|
||||
Self { metrics }
|
||||
}
|
||||
|
||||
/// Auxiliary function to convert [`MetricStatistics`] into a [`JsonValue`] array.
|
||||
pub fn to_json_array(&self) -> JsonValue {
|
||||
JsonValue::Array(vec![
|
||||
JsonValue::Number(self.metrics.avg_total_gas_used() as f64),
|
||||
JsonValue::Number(self.metrics.total_gas.min as f64),
|
||||
JsonValue::Number(self.metrics.total_gas.max as f64),
|
||||
JsonValue::Number(self.metrics.avg_wasm_gas_used() as f64),
|
||||
JsonValue::Number(self.metrics.wasm_gas.min as f64),
|
||||
JsonValue::Number(self.metrics.wasm_gas.max as f64),
|
||||
JsonValue::Number(self.metrics.avg_zk_circuits_gas_used() as f64),
|
||||
JsonValue::Number(self.metrics.zk_circuits_gas.min as f64),
|
||||
JsonValue::Number(self.metrics.zk_circuits_gas.max as f64),
|
||||
JsonValue::Number(self.metrics.avg_signatures_gas_used() as f64),
|
||||
JsonValue::Number(self.metrics.signatures_gas.min as f64),
|
||||
JsonValue::Number(self.metrics.signatures_gas.max as f64),
|
||||
JsonValue::Number(self.metrics.avg_deployments_gas_used() as f64),
|
||||
JsonValue::Number(self.metrics.deployments_gas.min as f64),
|
||||
JsonValue::Number(self.metrics.deployments_gas.max as f64),
|
||||
JsonValue::Number(self.metrics.timestamp.inner() as f64),
|
||||
])
|
||||
}
|
||||
}
|
||||
impl ExplorerService {
|
||||
/// Fetches the latest [`BaseStatistics`] from the explorer database, or returns `None` if no block exists.
|
||||
pub fn get_base_statistics(&self) -> Result<Option<BaseStatistics>> {
|
||||
let last_block = self.last_block();
|
||||
Ok(last_block
|
||||
// Throw database error if last_block retrievals fails
|
||||
.map_err(|e| {
|
||||
Error::DatabaseError(format!(
|
||||
"[get_base_statistics] Retrieving last block failed: {:?}",
|
||||
e
|
||||
))
|
||||
})?
|
||||
// Calculate base statistics and return result
|
||||
.map(|(height, header_hash)| {
|
||||
let epoch = block_epoch(height);
|
||||
let total_blocks = self.get_block_count();
|
||||
let total_txs = self.get_transaction_count();
|
||||
BaseStatistics { height, epoch, last_block: header_hash, total_blocks, total_txs }
|
||||
}))
|
||||
}
|
||||
|
||||
/// Fetches the latest metrics from the explorer database, returning a vector of
|
||||
/// [`MetricStatistics`] if found, or an empty Vec if no metrics exist.
|
||||
pub async fn get_metrics_statistics(&self) -> Result<Vec<MetricStatistics>> {
|
||||
// Fetch all metrics from the metrics store, handling any potential errors
|
||||
let metrics = self.db.metrics_store.get_all_metrics().map_err(|e| {
|
||||
Error::DatabaseError(format!(
|
||||
"[get_metrics_statistics] Retrieving metrics failed: {:?}",
|
||||
e
|
||||
))
|
||||
})?;
|
||||
|
||||
// Transform the fetched metrics into `MetricStatistics`, collect them into a vector
|
||||
let metric_statistics =
|
||||
metrics.iter().map(|metrics| MetricStatistics::new(metrics.clone())).collect();
|
||||
|
||||
Ok(metric_statistics)
|
||||
}
|
||||
|
||||
/// Fetches the latest metrics from the explorer database, returning [`MetricStatistics`] if found,
|
||||
/// or zero-initialized defaults when not.
|
||||
pub async fn get_latest_metrics_statistics(&self) -> Result<MetricStatistics> {
|
||||
// Fetch the latest metrics, handling any potential errors
|
||||
match self.db.metrics_store.get_last().map_err(|e| {
|
||||
Error::DatabaseError(format!(
|
||||
"[get_metrics_statistics] Retrieving latest metrics failed: {:?}",
|
||||
e
|
||||
))
|
||||
})? {
|
||||
// Transform metrics into `MetricStatistics` when found
|
||||
Some((_, metrics)) => Ok(MetricStatistics::new(metrics)),
|
||||
// Return default statistics when no metrics exist
|
||||
None => Ok(MetricStatistics::default()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
/* This file is part of DarkFi (https://dark.fi)
|
||||
*
|
||||
* Copyright (C) 2020-2025 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/>.
|
||||
*/
|
||||
|
||||
/// Initializes logging for test cases, which is useful for debugging issues encountered during testing.
|
||||
/// The logger is configured based on the provided list of targets to ignore and the desired log level.
|
||||
#[cfg(test)]
|
||||
pub fn init_logger(log_level: simplelog::LevelFilter, ignore_targets: Vec<&str>) {
|
||||
let mut cfg = simplelog::ConfigBuilder::new();
|
||||
|
||||
// Add targets to ignore
|
||||
for target in ignore_targets {
|
||||
cfg.add_filter_ignore(target.to_string());
|
||||
}
|
||||
|
||||
// Set log level
|
||||
cfg.set_target_level(log_level);
|
||||
|
||||
// initialize the logger
|
||||
if simplelog::TermLogger::init(
|
||||
log_level,
|
||||
cfg.build(),
|
||||
simplelog::TerminalMode::Mixed,
|
||||
simplelog::ColorChoice::Auto,
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
// Print an error message if logger failed to initialize
|
||||
eprintln!("Logger failed to initialize");
|
||||
}
|
||||
}
|
||||
@@ -1,492 +0,0 @@
|
||||
/* This file is part of DarkFi (https://dark.fi)
|
||||
*
|
||||
* Copyright (C) 2020-2025 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/>.
|
||||
*/
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use log::{debug, error};
|
||||
use smol::io::Cursor;
|
||||
use tinyjson::JsonValue;
|
||||
|
||||
use darkfi::{
|
||||
blockchain::{
|
||||
BlockInfo, BlockchainOverlay, HeaderHash, SLED_PENDING_TX_ORDER_TREE, SLED_PENDING_TX_TREE,
|
||||
SLED_TX_LOCATION_TREE, SLED_TX_TREE,
|
||||
},
|
||||
error::TxVerifyFailed,
|
||||
runtime::vm_runtime::Runtime,
|
||||
tx::Transaction,
|
||||
util::time::Timestamp,
|
||||
validator::fees::{circuit_gas_use, GasData, PALLAS_SCHNORR_SIGNATURE_FEE},
|
||||
zk::VerifyingKey,
|
||||
Error, Result,
|
||||
};
|
||||
use darkfi_sdk::{
|
||||
crypto::{ContractId, PublicKey},
|
||||
deploy::DeployParamsV1,
|
||||
pasta::pallas,
|
||||
tx::TransactionHash,
|
||||
};
|
||||
use darkfi_serial::{deserialize_async, serialize_async, AsyncDecodable, AsyncEncodable};
|
||||
|
||||
use crate::ExplorerService;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
/// Structure representing a `TRANSACTIONS_TABLE` record.
|
||||
pub struct TransactionRecord {
|
||||
/// Transaction hash identifier
|
||||
pub transaction_hash: String,
|
||||
/// Header hash identifier of the block this transaction was included in
|
||||
pub header_hash: String,
|
||||
// TODO: Split the payload into a more easily readable fields
|
||||
/// Transaction payload
|
||||
pub payload: Transaction,
|
||||
/// Time transaction was added to the block
|
||||
pub timestamp: Timestamp,
|
||||
/// Total gas used for processing transaction
|
||||
pub total_gas_used: u64,
|
||||
/// Gas used by WASM
|
||||
pub wasm_gas_used: u64,
|
||||
/// Gas used by ZK circuit operations
|
||||
pub zk_circuit_gas_used: u64,
|
||||
/// Gas used for creating the transaction signature
|
||||
pub signature_gas_used: u64,
|
||||
/// Gas used for deployments
|
||||
pub deployment_gas_used: u64,
|
||||
}
|
||||
|
||||
impl TransactionRecord {
|
||||
/// Auxiliary function to convert a `TransactionRecord` into a `JsonValue` array.
|
||||
pub fn to_json_array(&self) -> JsonValue {
|
||||
JsonValue::Array(vec![
|
||||
JsonValue::String(self.transaction_hash.clone()),
|
||||
JsonValue::String(self.header_hash.clone()),
|
||||
JsonValue::String(format!("{:?}", self.payload)),
|
||||
JsonValue::String(self.timestamp.to_string()),
|
||||
JsonValue::Number(self.total_gas_used as f64),
|
||||
JsonValue::Number(self.wasm_gas_used as f64),
|
||||
JsonValue::Number(self.zk_circuit_gas_used as f64),
|
||||
JsonValue::Number(self.signature_gas_used as f64),
|
||||
JsonValue::Number(self.deployment_gas_used as f64),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
impl ExplorerService {
|
||||
/// Resets transactions in the database by clearing transaction-related trees, returning an Ok result on success.
|
||||
pub fn reset_transactions(&self) -> Result<()> {
|
||||
// Initialize transaction trees to reset
|
||||
let trees_to_reset =
|
||||
[SLED_TX_TREE, SLED_TX_LOCATION_TREE, SLED_PENDING_TX_TREE, SLED_PENDING_TX_ORDER_TREE];
|
||||
|
||||
// Iterate over each associated transaction tree and delete its contents
|
||||
for tree_name in &trees_to_reset {
|
||||
let tree = &self.db.blockchain.sled_db.open_tree(tree_name)?;
|
||||
tree.clear()?;
|
||||
let tree_name_str = std::str::from_utf8(tree_name)?;
|
||||
debug!(target: "blockchain-explorer::blocks", "Successfully reset transaction tree: {tree_name_str}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Provides the transaction count of all the transactions in the explorer database.
|
||||
pub fn get_transaction_count(&self) -> usize {
|
||||
self.db.blockchain.txs_len()
|
||||
}
|
||||
|
||||
/// Fetches all known transactions from the database.
|
||||
///
|
||||
/// This function retrieves all transactions stored in the database and transforms
|
||||
/// them into a vector of [`TransactionRecord`]s. If no transactions are found,
|
||||
/// it returns an empty vector.
|
||||
pub fn get_transactions(&self) -> Result<Vec<TransactionRecord>> {
|
||||
// Retrieve all transactions and handle any errors encountered
|
||||
let txs = self.db.blockchain.transactions.get_all().map_err(|e| {
|
||||
Error::DatabaseError(format!("[get_transactions] Trxs retrieval: {e:?}"))
|
||||
})?;
|
||||
|
||||
// Transform the found `Transactions` into a vector of `TransactionRecords`
|
||||
let txs_records = txs
|
||||
.iter()
|
||||
.map(|(_, tx)| self.to_tx_record(None, tx))
|
||||
.collect::<Result<Vec<TransactionRecord>>>()?;
|
||||
|
||||
Ok(txs_records)
|
||||
}
|
||||
|
||||
/// Fetches all transactions from the database for the given block `header_hash`.
|
||||
///
|
||||
/// This function retrieves all transactions associated with the specified
|
||||
/// block header hash. It first parses the header hash and then fetches
|
||||
/// the corresponding [`BlockInfo`]. If the block is found, it transforms its
|
||||
/// transactions into a vector of [`TransactionRecord`]s. If no transactions
|
||||
/// are found, it returns an empty vector.
|
||||
pub fn get_transactions_by_header_hash(
|
||||
&self,
|
||||
header_hash: &str,
|
||||
) -> Result<Vec<TransactionRecord>> {
|
||||
// Parse header hash, returning an error if parsing fails
|
||||
let header_hash = header_hash
|
||||
.parse::<HeaderHash>()
|
||||
.map_err(|_| Error::ParseFailed("[get_transactions_by_header_hash] Invalid hash"))?;
|
||||
|
||||
// Fetch block by hash and handle encountered errors
|
||||
let block = match self.db.blockchain.get_blocks_by_hash(&[header_hash]) {
|
||||
Ok(blocks) => blocks.first().cloned().unwrap(),
|
||||
Err(Error::BlockNotFound(_)) => return Ok(vec![]),
|
||||
Err(e) => {
|
||||
return Err(Error::DatabaseError(format!(
|
||||
"[get_transactions_by_header_hash] Block retrieval failed: {e:?}"
|
||||
)))
|
||||
}
|
||||
};
|
||||
|
||||
// Transform block transactions into transaction records
|
||||
block
|
||||
.txs
|
||||
.iter()
|
||||
.map(|tx| self.to_tx_record(self.get_block_info(block.header.hash())?, tx))
|
||||
.collect::<Result<Vec<TransactionRecord>>>()
|
||||
}
|
||||
|
||||
/// Fetches a transaction given its header hash.
|
||||
///
|
||||
/// This function retrieves the transaction associated with the provided
|
||||
/// [`TransactionHash`] and transforms it into a [`TransactionRecord`] if found.
|
||||
/// If no transaction is found, it returns `None`.
|
||||
pub fn get_transaction_by_hash(
|
||||
&self,
|
||||
tx_hash: &TransactionHash,
|
||||
) -> Result<Option<TransactionRecord>> {
|
||||
let tx_store = &self.db.blockchain.transactions;
|
||||
|
||||
// Attempt to retrieve the transaction using the provided hash handling any potential errors
|
||||
let tx_opt = &tx_store.get(&[*tx_hash], false).map_err(|e| {
|
||||
Error::DatabaseError(format!(
|
||||
"[get_transaction_by_hash] Transaction retrieval failed: {e:?}"
|
||||
))
|
||||
})?[0];
|
||||
|
||||
// Transform `Transaction` to a `TransactionRecord`, returning None if no transaction was found
|
||||
tx_opt.as_ref().map(|tx| self.to_tx_record(None, tx)).transpose()
|
||||
}
|
||||
|
||||
/// Fetches the [`BlockInfo`] associated with a given transaction hash.
|
||||
///
|
||||
/// This auxiliary function first fetches the location of the transaction in the blockchain.
|
||||
/// If the location is found, it retrieves the associated [`HeaderHash`] and then fetches
|
||||
/// the block information corresponding to that header hash. The function returns the
|
||||
/// [`BlockInfo`] if successful, or `None` if no location or header hash is found.
|
||||
fn get_tx_block_info(&self, tx_hash: &TransactionHash) -> Result<Option<BlockInfo>> {
|
||||
// Retrieve the location of the transaction
|
||||
let location =
|
||||
self.db.blockchain.transactions.get_location(&[*tx_hash], false).map_err(|e| {
|
||||
Error::DatabaseError(format!(
|
||||
"[get_tx_block_info] Location retrieval failed: {e:?}"
|
||||
))
|
||||
})?[0];
|
||||
|
||||
// Fetch the `HeaderHash` associated with the location
|
||||
let header_hash = match location {
|
||||
None => return Ok(None),
|
||||
Some((block_height, _)) => {
|
||||
self.db.blockchain.blocks.get_order(&[block_height], false).map_err(|e| {
|
||||
Error::DatabaseError(format!(
|
||||
"[get_tx_block_info] Block retrieval failed: {e:?}"
|
||||
))
|
||||
})?[0]
|
||||
}
|
||||
};
|
||||
|
||||
// Return the associated `BlockInfo` if the header hash is found; otherwise, return `None`.
|
||||
match header_hash {
|
||||
None => Ok(None),
|
||||
Some(header_hash) => self.get_block_info(header_hash).map_err(|e| {
|
||||
Error::DatabaseError(format!(
|
||||
"[get_tx_block_info] BlockInfo retrieval failed: {e:?}"
|
||||
))
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetches the [`BlockInfo`] associated with a given [`HeaderHash`].
|
||||
///
|
||||
/// This auxiliary function attempts to retrieve the block information using
|
||||
/// the specified [`HeaderHash`]. It returns the associated [`BlockInfo`] if found,
|
||||
/// or `None` when not found.
|
||||
fn get_block_info(&self, header_hash: HeaderHash) -> Result<Option<BlockInfo>> {
|
||||
match self.db.blockchain.get_blocks_by_hash(&[header_hash]) {
|
||||
Err(Error::BlockNotFound(_)) => Ok(None),
|
||||
Ok(block_info) => Ok(block_info.into_iter().next()),
|
||||
Err(e) => Err(Error::DatabaseError(format!(
|
||||
"[get_transactions_by_header_hash] Block retrieval failed: {e:?}"
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculates the gas data for a given transaction, returning a [`GasData`] instance detailing
|
||||
/// various aspects of the gas usage.
|
||||
pub async fn calculate_tx_gas_data(
|
||||
&self,
|
||||
tx: &Transaction,
|
||||
verify_fee: bool,
|
||||
) -> Result<GasData> {
|
||||
let tx_hash = tx.hash();
|
||||
|
||||
let overlay = BlockchainOverlay::new(&self.db.blockchain)?;
|
||||
|
||||
// Gas accumulators
|
||||
let mut total_gas_used = 0;
|
||||
let mut zk_circuit_gas_used = 0;
|
||||
let mut wasm_gas_used = 0;
|
||||
let mut deploy_gas_used = 0;
|
||||
let mut gas_paid = 0;
|
||||
|
||||
// Table of public inputs used for ZK proof verification
|
||||
let mut zkp_table = vec![];
|
||||
// Table of public keys used for signature verification
|
||||
let mut sig_table = vec![];
|
||||
|
||||
// Index of the Fee-paying call
|
||||
let fee_call_idx = 0;
|
||||
|
||||
// Map of ZK proof verifying keys for the transaction
|
||||
let mut verifying_keys: HashMap<[u8; 32], HashMap<String, VerifyingKey>> = HashMap::new();
|
||||
for call in &tx.calls {
|
||||
verifying_keys.insert(call.data.contract_id.to_bytes(), HashMap::new());
|
||||
}
|
||||
|
||||
let block_target = self.db.blockchain.blocks.get_last()?.0 + 1;
|
||||
|
||||
// We'll also take note of all the circuits in a Vec so we can calculate their verification cost.
|
||||
let mut circuits_to_verify = vec![];
|
||||
|
||||
// Iterate over all calls to get the metadata
|
||||
for (idx, call) in tx.calls.iter().enumerate() {
|
||||
// Transaction must not contain a Money::PoWReward(0x02) call
|
||||
if call.data.is_money_pow_reward() {
|
||||
error!(target: "block_explorer::calculate_tx_gas_data", "Reward transaction detected");
|
||||
return Err(TxVerifyFailed::ErroneousTxs(vec![tx.clone()]).into())
|
||||
}
|
||||
|
||||
// Write the actual payload data
|
||||
let mut payload = vec![];
|
||||
tx.calls.encode_async(&mut payload).await?;
|
||||
|
||||
let wasm = overlay.lock().unwrap().contracts.get(call.data.contract_id)?;
|
||||
|
||||
let mut runtime = Runtime::new(
|
||||
&wasm,
|
||||
overlay.clone(),
|
||||
call.data.contract_id,
|
||||
block_target,
|
||||
block_target,
|
||||
tx_hash,
|
||||
idx as u8,
|
||||
)?;
|
||||
|
||||
let metadata = runtime.metadata(&payload)?;
|
||||
|
||||
// Decode the metadata retrieved from the execution
|
||||
let mut decoder = Cursor::new(&metadata);
|
||||
|
||||
// The tuple is (zkas_ns, public_inputs)
|
||||
let zkp_pub: Vec<(String, Vec<pallas::Base>)> =
|
||||
AsyncDecodable::decode_async(&mut decoder).await?;
|
||||
let sig_pub: Vec<PublicKey> = AsyncDecodable::decode_async(&mut decoder).await?;
|
||||
|
||||
if decoder.position() != metadata.len() as u64 {
|
||||
error!(
|
||||
target: "block_explorer::calculate_tx_gas_data",
|
||||
"[BLOCK_EXPLORER] Failed decoding entire metadata buffer for {}:{}", tx_hash, idx,
|
||||
);
|
||||
return Err(TxVerifyFailed::ErroneousTxs(vec![tx.clone()]).into())
|
||||
}
|
||||
|
||||
// Here we'll look up verifying keys and insert them into the per-contract map.
|
||||
for (zkas_ns, _) in &zkp_pub {
|
||||
let inner_vk_map =
|
||||
verifying_keys.get_mut(&call.data.contract_id.to_bytes()).unwrap();
|
||||
|
||||
// TODO: This will be a problem in case of ::deploy, unless we force a different
|
||||
// namespace and disable updating existing circuit. Might be a smart idea to do
|
||||
// so in order to have to care less about being able to verify historical txs.
|
||||
if inner_vk_map.contains_key(zkas_ns.as_str()) {
|
||||
continue
|
||||
}
|
||||
|
||||
let (zkbin, vk) =
|
||||
overlay.lock().unwrap().contracts.get_zkas(&call.data.contract_id, zkas_ns)?;
|
||||
|
||||
inner_vk_map.insert(zkas_ns.to_string(), vk);
|
||||
circuits_to_verify.push(zkbin);
|
||||
}
|
||||
|
||||
zkp_table.push(zkp_pub);
|
||||
sig_table.push(sig_pub);
|
||||
|
||||
// Contracts are not included within blocks. They need to be deployed off-chain so that they can be accessed and utilized for fee data computation
|
||||
if call.data.is_deployment()
|
||||
/* DeployV1 */
|
||||
{
|
||||
// Deserialize the deployment parameters
|
||||
let deploy_params: DeployParamsV1 = deserialize_async(&call.data.data[1..]).await?;
|
||||
let deploy_cid = ContractId::derive_public(deploy_params.public_key);
|
||||
|
||||
// Instantiate the new deployment runtime
|
||||
let mut deploy_runtime = Runtime::new(
|
||||
&deploy_params.wasm_bincode,
|
||||
overlay.clone(),
|
||||
deploy_cid,
|
||||
block_target,
|
||||
block_target,
|
||||
tx_hash,
|
||||
idx as u8,
|
||||
)?;
|
||||
|
||||
deploy_runtime.deploy(&deploy_params.ix)?;
|
||||
|
||||
deploy_gas_used = deploy_runtime.gas_used();
|
||||
|
||||
// Append the used deployment gas
|
||||
total_gas_used += deploy_gas_used;
|
||||
}
|
||||
|
||||
// At this point we're done with the call and move on to the next one.
|
||||
// Accumulate the WASM gas used.
|
||||
wasm_gas_used = runtime.gas_used();
|
||||
|
||||
// Append the used wasm gas
|
||||
total_gas_used += wasm_gas_used;
|
||||
}
|
||||
|
||||
// The signature fee is tx_size + fixed_sig_fee * n_signatures
|
||||
let signature_gas_used = (PALLAS_SCHNORR_SIGNATURE_FEE * tx.signatures.len() as u64) +
|
||||
serialize_async(tx).await.len() as u64;
|
||||
|
||||
// Append the used signature gas
|
||||
total_gas_used += signature_gas_used;
|
||||
|
||||
// The ZK circuit fee is calculated using a function in validator/fees.rs
|
||||
for zkbin in circuits_to_verify.iter() {
|
||||
zk_circuit_gas_used = circuit_gas_use(zkbin);
|
||||
|
||||
// Append the used zk circuit gas
|
||||
total_gas_used += zk_circuit_gas_used;
|
||||
}
|
||||
|
||||
if verify_fee {
|
||||
// Deserialize the fee call to find the paid fee
|
||||
let fee: u64 = match deserialize_async(&tx.calls[fee_call_idx].data.data[1..9]).await {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
error!(
|
||||
target: "block_explorer::calculate_tx_gas_data",
|
||||
"[VALIDATOR] Failed deserializing tx {} fee call: {}", tx_hash, e,
|
||||
);
|
||||
return Err(TxVerifyFailed::InvalidFee.into())
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: This counts 1 gas as 1 token unit. Pricing should be better specified.
|
||||
// Check that enough fee has been paid for the used gas in this transaction.
|
||||
if total_gas_used > fee {
|
||||
error!(
|
||||
target: "block_explorer::calculate_tx_gas_data",
|
||||
"[VALIDATOR] Transaction {} has insufficient fee. Required: {}, Paid: {}",
|
||||
tx_hash, total_gas_used, fee,
|
||||
);
|
||||
return Err(TxVerifyFailed::InsufficientFee.into())
|
||||
}
|
||||
debug!(target: "block_explorer::calculate_tx_gas_data", "The gas paid for transaction {}: {}", tx_hash, gas_paid);
|
||||
|
||||
// Store paid fee
|
||||
gas_paid = fee;
|
||||
}
|
||||
|
||||
// Commit changes made to the overlay
|
||||
overlay.lock().unwrap().overlay.lock().unwrap().apply()?;
|
||||
|
||||
let fee_data = GasData {
|
||||
paid: gas_paid,
|
||||
wasm: wasm_gas_used,
|
||||
zk_circuits: zk_circuit_gas_used,
|
||||
signatures: signature_gas_used,
|
||||
deployments: deploy_gas_used,
|
||||
};
|
||||
|
||||
debug!(target: "block_explorer::calculate_tx_gas_data", "The total gas usage for transaction {}: {:?}", tx_hash, fee_data);
|
||||
|
||||
Ok(fee_data)
|
||||
}
|
||||
|
||||
/// Converts a [`Transaction`] and its associated block information into a [`TransactionRecord`].
|
||||
///
|
||||
/// This auxiliary function first retrieves the gas data associated with the provided transaction.
|
||||
/// If [`BlockInfo`] is not provided, it attempts to fetch it using the transaction's hash,
|
||||
/// returning an error if the block information cannot be found. Upon success, the function
|
||||
/// returns a [`TransactionRecord`] containing relevant details about the transaction.
|
||||
fn to_tx_record(
|
||||
&self,
|
||||
block_info_opt: Option<BlockInfo>,
|
||||
tx: &Transaction,
|
||||
) -> Result<TransactionRecord> {
|
||||
// Fetch the gas data associated with the transaction
|
||||
let gas_data_option = self.db.metrics_store.get_tx_gas_data(&tx.hash()).map_err(|e| {
|
||||
Error::DatabaseError(format!(
|
||||
"[to_tx_record] Failed to fetch the gas data associated with transaction {}: {e:?}",
|
||||
tx.hash()
|
||||
))
|
||||
})?;
|
||||
|
||||
// Unwrap the option, providing a default value when `None`
|
||||
let gas_data = gas_data_option.unwrap_or_else(GasData::default);
|
||||
|
||||
// Process provided block_info option
|
||||
let block_info = match block_info_opt {
|
||||
// Use provided block_info when present
|
||||
Some(block_info) => block_info,
|
||||
// Fetch the block info associated with the transaction when block info not provided
|
||||
None => {
|
||||
match self.get_tx_block_info(&tx.hash())? {
|
||||
Some(block_info) => block_info,
|
||||
// If no associated block info found, throw an error as this should not happen
|
||||
None => {
|
||||
return Err(Error::BlockNotFound(format!(
|
||||
"[to_tx_record] Required `BlockInfo` was not found for transaction: {}",
|
||||
tx.hash()
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Return transformed transaction record
|
||||
Ok(TransactionRecord {
|
||||
transaction_hash: tx.hash().to_string(),
|
||||
header_hash: block_info.hash().to_string(),
|
||||
timestamp: block_info.header.timestamp,
|
||||
payload: tx.clone(),
|
||||
total_gas_used: gas_data.total_gas_used(),
|
||||
wasm_gas_used: gas_data.wasm,
|
||||
zk_circuit_gas_used: gas_data.zk_circuits,
|
||||
signature_gas_used: gas_data.signatures,
|
||||
deployment_gas_used: gas_data.deployments,
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user