├── .env.example ├── .gitignore ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── bitdata ├── __init__.py ├── analysis │ ├── addresses.py │ ├── coinbase.py │ ├── mining.py │ └── taproot.py ├── core │ ├── __init__.py │ └── config.py ├── notifiers │ ├── discord.py │ └── telegram.py └── provider │ ├── __init__.py │ ├── bitcoin_rpc.py │ ├── blockstream.py │ ├── mempool.py │ └── quiknode.py ├── dashboard ├── On-chain.py ├── bitpolito_logo.png ├── lightning.gif └── pages │ └── Lightning_Network.py ├── grafana ├── docker-compose.yml ├── exporter │ ├── Dockerfile │ ├── btc_conf.py │ └── client.py ├── grafana │ ├── Dockerfile │ └── config │ │ └── grafana.ini ├── prometheus │ ├── Dockerfile │ └── config │ │ └── prometheus.yml └── readme.md ├── pyproject.toml ├── requirements.txt ├── setup.py └── tests ├── conftest.py ├── test_blockstream.py └── test_btc_rpc.py /.env.example: -------------------------------------------------------------------------------- 1 | RPC_USER='bitpolito' 2 | RPC_PASSWORD='bitpolito' 3 | RPC_HOST='localhost' 4 | RPC_PORT=8332 5 | 6 | BOT_TOKEN="XXX" 7 | CHAT_ID="XXX" 8 | DISCORD_WEBHOOK_URL="XXX" 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | poetry.lock 132 | 133 | 134 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.10-slim 2 | RUN apt-get update 3 | RUN apt-get install -y curl python3-dev autoconf 4 | RUN curl -sSL https://install.python-poetry.org | python3 - 5 | ENV PATH="/root/.local/bin:$PATH" 6 | WORKDIR /app 7 | COPY pyproject.toml poetry.lock ./ 8 | RUN poetry config virtualenvs.create false 9 | RUN poetry install --no-dev --no-root 10 | 11 | COPY . . 12 | EXPOSE 8501 13 | 14 | CMD ["poetry", "run", "streamlit", "run", "dashboard/On-chain.py"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 BitPolito 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | frontend: 2 | poetry run streamlit run dashboard/On-chain.py 3 | 4 | test: 5 | pytest tests/ 6 | 7 | isort: 8 | poetry run isort --profile black . 9 | 10 | black: 11 | poetry run black . 12 | 13 | format: 14 | make isort 15 | make black 16 | make mypy 17 | 18 | mypy: 19 | poetry run mypy bitdata --ignore-missing 20 | 21 | clean: 22 | rm -r bitdata.egg-info/ || true 23 | find . -name ".DS_Store" -exec rm -f {} \; || true 24 | rm -rf dist || true 25 | rm -rf build || true 26 | 27 | package: 28 | poetry export -f requirements.txt --without-hashes --output requirements.txt 29 | make clean 30 | python setup.py sdist bdist_wheel 31 | 32 | test: 33 | pytest tests/ 34 | 35 | install: 36 | make clean 37 | python setup.py sdist bdist_wheel 38 | pip install --upgrade dist/* 39 | 40 | upload: 41 | make clean 42 | python setup.py sdist bdist_wheel 43 | twine upload --repository pypi dist/* 44 | 45 | docker-build: 46 | docker build -t bitdata . 47 | 48 | docker-run: 49 | docker run -p 8501:8501 -v ./frontend:/app/frontend bitdata 50 | 51 | docker-stop: 52 | -docker stop bitdata 53 | -docker rm bitdata 54 | 55 | docker: 56 | -make docker-stop 57 | -make docker-build 58 | -make docker-run 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bitcoin Data Analysis 2 | 3 | [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/your-username/bitcoin-data-analysis/blob/main/LICENSE) 4 | 5 | ## Overview 6 | 7 | The Bitcoin Data Analysis is a Python library designed to facilitate the analysis of Bitcoin on-chain data and Lightning Network data. It provides various functionalities and data providers to retrieve, process, and analyze Bitcoin-related information. 8 | 9 | The library consists of the following components: 10 | 11 | - **bitdata**: This module contains different providers to fetch Bitcoin data and some functions to help analysis. 12 | - **dashboard**: This folder contains a Streamlit web page for visualizing and interacting with the analyzed data. 13 | 14 | 15 | ### Installation 16 | 17 | There are different ways to install the library. 18 | 19 | 26 | 27 | 28 | Clone the repository: 29 | 30 | ```bash 31 | git clone https://github.com/BitPolito/bitcoin-data-analysis 32 | cd bitcoin-data-analysis 33 | ``` 34 | 35 | #### Docker 36 | If you don't have Docker installed, you can follow the instructions [here](https://docs.docker.com/get-docker/). 37 | 38 | Build and run the docker image with: 39 | 40 | ```bash 41 | make docker 42 | ``` 43 | Access the [streamlit](https://streamlit.io/) web page in your browser at http://localhost:8501. 44 | 45 | #### Poetry 46 | If you don't have poetry installed, follow the instructions in the [official Poetry documentation](https://python-poetry.org/docs/#installation) to install Poetry for your operating system. 47 | 48 | 49 | Install python libraries 50 | ``` 51 | poetry install 52 | ``` 53 | ### Config 54 | Add your own configuration file in the root folder of the project. 55 | You can use the .env.example file as a template. 56 | 57 | ```bash 58 | cp .env.example .env 59 | # edit .env file with your configuration 60 | nano .env 61 | ``` 62 | ## Data Dashboard 63 | 64 | Webpage built with streamlit, that displays some live statistics about bitcoin network. Try it [here](https://bumblebee00-data-analysis-on-chain-kk5uep.streamlit.app/) 65 | 66 | ### Run the dashboard 67 | ``` 68 | poetry run streamlit run dashboard/On-chain.py 69 | ``` 70 | 71 | Access the [streamlit](https://streamlit.io/) web page in your browser at http://localhost:8501. 72 | 73 | 74 | ## BitData - Analysis 75 | 76 | Some examples tools and script to analyze bitcoin data. 77 | 78 | ### Coinbase String Monitor 79 | This script analyze the coinbase of the last 10 blocks of the testnet, if it found the target string on the coinbase transaction will send a message in a telegram channel. 80 | Will continue to analyze new blocks every 30 seconds. 81 | 82 | - Change BOT_TOKEN and CHAT_ID in the .env file to enable the telegram bot 83 | - The bot should be added to the channel as an administrator. The CHAT_ID is the chat of the bot with the channel. 84 | - Change DISCORD_WEBHOOK_URL in the .env file to enable the discord bot 85 | - The bot should be created as explained [here](https://discord.com/developers/docs/getting-started) and added with the right priviledges in the server. At this point the webhook url can be exctracted from the sub-channel that you want the bot will notify into. 86 | 87 | ``` 88 | poetry run python -m bitdata.analysis.coinbase -n testnet -t "Stratum v2" -p 10 89 | ``` 90 | 91 | 92 | ### Mining pool distribution 93 | 94 | ```bash 95 | poetry run python -m bitdata.analysis.mining 96 | ``` 97 | 98 | ### Transactions per block 99 | ```bash 100 | poetry run python -m bitdata.analysis.addresses 101 | ``` 102 | 103 | ### Taproot transaction count 104 | ```bash 105 | poetry run python -m bitdata.analysis.taproot 106 | ``` 107 | 108 | ## Contributing 109 | 110 | Contributions to the Bitcoin Data Analysis Library are welcome! If you encounter any issues, have feature suggestions, or would like to contribute code, feel free to open an issue or submit a pull request. 111 | 112 | 113 | 114 | ## License 115 | 116 | The Bitcoin Data Analysis Library is open source and released under the [MIT License](https://github.com/your-username/bitcoin-data-analysis/blob/main/LICENSE). 117 | 118 | 119 | ## Acknowledgements 120 | We would like to acknowledge the following resources and libraries that have contributed to the development of this project: 121 | 122 | [bitnodes.io](https://bitnodes.io/) 123 | [blockchain.info](https://www.blockchain.info) 124 | [bloackstream.info](https://blockstream.info) 125 | 126 | 127 | -------------------------------------------------------------------------------- /bitdata/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitPolito/bitcoin-data-analysis/404e705fe2bc8e6c900e75a3ac28bd27588b2bc4/bitdata/__init__.py -------------------------------------------------------------------------------- /bitdata/analysis/addresses.py: -------------------------------------------------------------------------------- 1 | from ..provider.bitcoin_rpc import BitcoinRPC 2 | 3 | 4 | def last_block_analytics(rpc_manager: BitcoinRPC): 5 | current_height = rpc_manager.get_last_block_height() 6 | last_block = rpc_manager.get_block_by_height(current_height) 7 | tx_count = len(last_block["tx"]) 8 | # Print the results 9 | print( 10 | "Number of Transactions in Block " + str(current_height) + ": " + str(tx_count) 11 | ) 12 | address_result = rpc_manager.address_block_analytics(last_block) 13 | address_types_count = address_result["address_types_count"] 14 | address_types_amount = address_result["address_types_amount"] 15 | 16 | print("\nNumber of UTXOs by address type:") 17 | for address_type, count in address_types_count.items(): 18 | print(f"{address_type}: {count}") 19 | 20 | print("\nAmount of UTXOs by address type:") 21 | for address_type, amount in address_types_amount.items(): 22 | print(f"{address_type}: {amount}") 23 | 24 | 25 | if __name__ == "__main__": 26 | from ..core.config import BitConfig 27 | 28 | cfg = BitConfig() 29 | rpc_manager = BitcoinRPC(cfg) 30 | last_block_analytics(rpc_manager) 31 | -------------------------------------------------------------------------------- /bitdata/analysis/coinbase.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import asyncio 3 | import time 4 | 5 | from bitdata.notifiers.telegram import TelegramWriter 6 | from bitdata.notifiers.discord import DiscordWriter 7 | from bitdata.provider.mempool import MempoolProvider 8 | 9 | # This script will listen for new blocks and check if the coinbase transaction contains the string "Stratum v2" 10 | # If it does, it will send a message to the Telegram channel 11 | # The raw transaction converted in text contains also the miner input (?) 12 | 13 | SLEEP_TIME = 30 # Time to wait in seconds between checks 14 | 15 | class CoinbaseAnalyzer: 16 | def __init__( 17 | self, 18 | provider, 19 | telegram_writer, 20 | discord_writer, 21 | target_string, 22 | network="testnet4", 23 | n_previous_blocks=0, 24 | ): 25 | self.provider = provider 26 | self.telegram_writer = telegram_writer 27 | self.discord_writer = discord_writer 28 | self.target_string = target_string 29 | self.last_hash = None 30 | self.network = network 31 | self.n_previous_blocks = n_previous_blocks 32 | 33 | def get_miner_text_input(self, raw_transaction): 34 | try: 35 | inputs = raw_transaction["result"]["vin"] 36 | script = inputs[0]["coinbase"] 37 | ascii_string = "" 38 | for i in range(0, len(script), 2): 39 | ascii_string += chr(int(script[i : i + 2], 16)) 40 | return ascii_string.lower() 41 | except Exception as e: 42 | # The raw transaction as string may contain the miner input 43 | return raw_transaction.lower() 44 | 45 | async def notify_message(self, block_height, block_hash): 46 | url = ( 47 | "https://mempool.space/it/testnet4/block/" 48 | if self.network == "testnet4" 49 | else "https://mempool.space/it/block/" 50 | ) 51 | message = f"""Found a new block from SRI Pool : **{self.target_string}** in {self.network} block: [@{block_height}]({url}{block_hash})""" 52 | await self.telegram_writer.send_telegram_message(message) 53 | await self.discord_writer.send_discord_message(message) 54 | 55 | async def check_new_block(self): 56 | last_hash = self.provider.last_hash() 57 | if last_hash == self.last_hash: 58 | return 59 | self.last_hash = last_hash 60 | # New block found 61 | last_height = self.provider.get_last_height() 62 | coinbase_raw_transaction = self.provider.get_raw_coinbase_transaction(last_hash) 63 | miner_input = self.get_miner_text_input(coinbase_raw_transaction) 64 | if self.target_string.lower() in miner_input: 65 | await self.notify_message(last_height, last_hash) 66 | else: 67 | print(f"New block: {last_height} - {self.last_hash}") 68 | 69 | async def check_from_previous_n_blocks(self): 70 | if self.n_previous_blocks <= 0: 71 | return 72 | list_of_blocks = self.provider.get_last_n_blocks(self.n_previous_blocks) 73 | for block in list_of_blocks[: self.n_previous_blocks]: 74 | #print(block) 75 | block_hash = block["id"] 76 | block_height = block["height"] 77 | coinbase_raw_transaction = self.provider.get_raw_coinbase_transaction( 78 | block_hash 79 | ) 80 | miner_input = self.get_miner_text_input(coinbase_raw_transaction) 81 | if self.target_string.lower() in miner_input: 82 | await self.notify_message(block_height, block_hash) 83 | 84 | async def run(self): 85 | await self.check_from_previous_n_blocks() 86 | while True: 87 | await self.check_new_block() 88 | await asyncio.sleep(SLEEP_TIME) # Wait for 10 seconds before checking again 89 | 90 | if __name__ == "__main__": 91 | parser = argparse.ArgumentParser(description="Block Analyzer Script") 92 | parser.add_argument( 93 | "--network", 94 | "-n", 95 | type=str, 96 | default="testnet4", 97 | help="Network (e.g., testnet or mainnet)", 98 | ) 99 | parser.add_argument( 100 | "--target", 101 | "-t", 102 | type=str, 103 | default="Stratum V2", 104 | help="Target string to search in miner input", 105 | ) 106 | parser.add_argument( 107 | "--previous", 108 | "-p", 109 | type=int, 110 | default=0, 111 | help="Number of previous blocks to check from", 112 | ) 113 | args = parser.parse_args() 114 | 115 | network = args.network 116 | target_string = args.target 117 | n_previous_blocks = args.previous 118 | provider = MempoolProvider(network=network) 119 | telegram_writer = TelegramWriter() 120 | discord_writer = DiscordWriter() 121 | coinbase_analyzer = CoinbaseAnalyzer( 122 | provider, telegram_writer, discord_writer, target_string, network, n_previous_blocks 123 | ) 124 | loop = asyncio.get_event_loop() 125 | loop.run_until_complete(coinbase_analyzer.run()) 126 | -------------------------------------------------------------------------------- /bitdata/analysis/mining.py: -------------------------------------------------------------------------------- 1 | import base64 2 | 3 | import pandas as pd 4 | 5 | from ..provider.bitcoin_rpc import BitcoinRPC 6 | 7 | 8 | # Iterate over the last 10 blocks and extract the required data. 9 | def mining_analytics(rpc_manager: BitcoinRPC, past_blocks: int = 10): 10 | block_data = [] 11 | current_height = rpc_manager.get_last_block_height() 12 | assert current_height is not None, "Unable to get last block height" 13 | assert ( 14 | past_blocks <= current_height 15 | ), "Past blocks must be less than or equal to the current height" 16 | 17 | for height in range(current_height, current_height - past_blocks, -1): 18 | # Get the block hash and information for the current height. 19 | block = rpc_manager.get_block_by_height(height) 20 | if block is None: 21 | print(f"Block at height {height} not found") 22 | continue 23 | # Get the coinbase transaction for the block. 24 | tx0 = block["tx"][0] 25 | coinbase_tx = rpc_manager.get_transaction(tx0) 26 | 27 | # Extract the value of the OP_RETURN output from the coinbase transaction for the block. 28 | op_return_value = None 29 | 30 | for output in coinbase_tx["vout"]: 31 | if output["scriptPubKey"]["type"] == "nulldata": 32 | op_return_value = output["scriptPubKey"]["asm"].split(" ")[1] 33 | break 34 | 35 | # Add the block data to the block data list. 36 | block_data.append( 37 | { 38 | "Height": height, 39 | "Timestamp": block["time"], 40 | "Transaction Count": len(block["tx"]), 41 | "BTC Fees": block.get("fee", 0), 42 | "Size (MB)": block["size"] / 1000000, 43 | "Branch ID": "Orphan" if block["confirmations"] == 0 else "Main", 44 | "Coinbase Transaction": coinbase_tx, 45 | "OP_RETURN": op_return_value, 46 | } 47 | ) 48 | 49 | # Map the OP_RETURN value to the mining operation and add it to the pandas dataframe. 50 | mining_ops = { 51 | "54686520496e7465726e65746f662042697420426f6e6473": "Unknown", 52 | "5765622050726f766f736b79": "Web Provosky", 53 | "416c6978612054726164696e67205465726d": "Alexa Trading", 54 | "4d696e656420426974636f696e": "Mined Bitcoin", 55 | "4e6f746172696f757320434f564944": "Notarious COVID", 56 | "496e66696e69747950726f6d6f74696f6e": "InfinityPromotion", 57 | "466f726d756c61205465726d": "FormulaTerm", 58 | "4269746d696e657273206f662046756c6c466f726365": "FullForce", 59 | "44696769626f7920426974636f696e": "Digibyte Pool", 60 | "426974486f6c6520486f6c64696e67": "BitHole Holding", 61 | "4c696768746e696e6720526f636b73": "Lightning Rocks", 62 | "52696768674d696e656420416c6c69616e6365": "RightMining Alliance", 63 | # '50696f6e657820576f726b73': 'Pionex', 64 | # '4269747374616d70': 'Bitstamp', 65 | "536c75736820506f6f6c": "Slush Pool", 66 | "4632506f6f6c": "F2Pool", 67 | "416e74706f6f6c": "Antpool", 68 | "566961425463": "ViaBTC", 69 | "4254632e636f6d": "BTC.com", 70 | "506f6f6c696e": "Poolin", 71 | "47656e65736973204d696e696e67": "Genesis Mining", 72 | "42697466757279": "Bitfury", 73 | "42696e616e636520506f6f6c": "Binance Pool", 74 | "4b616e6f20506f6f6c": "Kano Pool", 75 | "636f696e62617365": "Coinbase", 76 | "4254432d474c": "BTCC Pool", 77 | "456c6967697573": "Eligius", 78 | "4b616e6f": "KanoPool", 79 | "5761746572746578": "Waterhole", 80 | } 81 | 82 | for block in block_data: 83 | op_return_value = block["OP_RETURN"] 84 | if op_return_value in mining_ops: 85 | block["Mining Operation"] = mining_ops[op_return_value] 86 | else: 87 | block["Mining Operation"] = "Unknown" 88 | 89 | # Create a pandas dataframe with the block data. 90 | block_df = pd.DataFrame(block_data) 91 | 92 | return block_df 93 | 94 | 95 | if __name__ == "__main__": 96 | from ..core.config import BitConfig 97 | 98 | cfg = BitConfig() 99 | rpc_manager = BitcoinRPC(cfg) 100 | df = mining_analytics(rpc_manager, 10) 101 | # Print the resulting dataframe. 102 | print(df) 103 | -------------------------------------------------------------------------------- /bitdata/analysis/taproot.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | import matplotlib.pyplot as plt 4 | 5 | from ..provider.bitcoin_rpc import BitcoinRPC 6 | 7 | # Define the taproot activation height 8 | TAPROOT_ACTIVATION_HEIGHT = 709632 9 | 10 | """ 11 | This Python code allows for the analysis of taproot transactions on the Bitcoin blockchain. 12 | It uses the bitcoinrpc library to connect to a local Bitcoin node via RPC credentials, and then retrieves and analyzes block data to determine the number of taproot transactions. 13 | The code continuously runs in the background, checking for new blocks and updating the plot accordingly. 14 | To use this code, a Bitcoin node running locally and RPC credentials set up are needed. bitcoinrpc and matplotlib libraries installed are necessary too. 15 | The plot shows the number of taproot transactions on the y-axis and the block height on the x-axis. The plot updates in real-time as new blocks are added to the blockchain, showing the trend in taproot transactions over time. 16 | Overall, this code provides a useful tool for analyzing the adoption and usage of taproot transactions on the Bitcoin blockchain. 17 | """ 18 | 19 | 20 | def taproot_counter(rpc_manager: BitcoinRPC): 21 | # Initialize the plot 22 | fig, ax = plt.subplots() 23 | ax.set_xlabel("Block Height") 24 | ax.set_ylabel("Taproot Transactions") 25 | ax.set_title("Taproot Transactions Since Activation") 26 | 27 | # Start analyzing blocks between current_height and taproot_activation_height 28 | current_height = rpc_manager.get_last_block_height() 29 | for i in range(current_height, TAPROOT_ACTIVATION_HEIGHT, -1): 30 | block = rpc_manager.get_block_by_height(i) 31 | tx_count = len(block["tx"]) 32 | taproot_tx_count = 0 33 | for txid in block["tx"]: 34 | tx = rpc_manager.get_transaction(txid) 35 | for output in tx["vout"]: 36 | if "taproot" in output["scriptPubKey"]["type"]: 37 | taproot_tx_count += 1 38 | break 39 | print( 40 | f"Block Height: {i}, Transactions: {tx_count}, Taproot Transactions: {taproot_tx_count}" 41 | ) 42 | ax.plot(i, taproot_tx_count, "bo") 43 | plt.draw() 44 | 45 | while True: 46 | # Check if a new blocks has been added to the blockchain 47 | new_height = rpc_manager.get_last_block_height() 48 | if new_height > current_height: 49 | # Analyze the new blocks 50 | for i in range(current_height + 1, new_height + 1): 51 | block = rpc_manager.get_block_by_height(i) 52 | tx_count = len(block["tx"]) 53 | taproot_tx_count = 0 54 | for txid in block["tx"]: 55 | tx = rpc_manager.get_transaction(txid) 56 | for output in tx["vout"]: 57 | if "taproot" in output["scriptPubKey"]["type"]: 58 | taproot_tx_count += 1 59 | break 60 | print( 61 | f"Block Height: {new_height}, Transactions: {tx_count}, Taproot Transactions: {taproot_tx_count}" 62 | ) 63 | ax.plot(i, taproot_tx_count, "bo") 64 | plt.draw() 65 | plt.pause(0.001) 66 | current_height = new_height 67 | time.sleep(1) 68 | 69 | 70 | if __name__ == "__main__": 71 | from ..core.config import BitConfig 72 | 73 | cfg = BitConfig() 74 | 75 | rpc_manager = BitcoinRPC(cfg) 76 | taproot_counter(rpc_manager) 77 | -------------------------------------------------------------------------------- /bitdata/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitPolito/bitcoin-data-analysis/404e705fe2bc8e6c900e75a3ac28bd27588b2bc4/bitdata/core/__init__.py -------------------------------------------------------------------------------- /bitdata/core/config.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseSettings 2 | 3 | 4 | class BitConfig(BaseSettings): 5 | RPC_USER: str 6 | RPC_PASSWORD: str 7 | RPC_HOST: str 8 | RPC_PORT: int 9 | 10 | class Config: 11 | env_file = "./.env" 12 | 13 | 14 | cfg = BitConfig() 15 | -------------------------------------------------------------------------------- /bitdata/notifiers/discord.py: -------------------------------------------------------------------------------- 1 | from discordwebhook import Discord 2 | import os 3 | from dotenv import load_dotenv 4 | 5 | load_dotenv() # take environment variables from .env. 6 | 7 | class DiscordWriter: 8 | def __init__(self): 9 | self.webhookurl = os.getenv("DISCORD_WEBHOOK_URL") 10 | self.discord = Discord(url = self.webhookurl) 11 | 12 | async def send_discord_message(self, message): 13 | self.discord.post(content=message) -------------------------------------------------------------------------------- /bitdata/notifiers/telegram.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from dotenv import load_dotenv 4 | from telegram import Bot 5 | from telegram.constants import ParseMode 6 | 7 | load_dotenv() # take environment variables from .env. 8 | 9 | 10 | class TelegramWriter: 11 | def __init__( 12 | self, 13 | ): 14 | self._token = os.getenv("BOT_TOKEN") 15 | self.chat_id = os.getenv("CHAT_ID") 16 | self.bot = Bot(token=self._token) 17 | 18 | async def send_telegram_message(self, message): 19 | if self.bot: 20 | await self.bot.send_message( 21 | chat_id=self.chat_id, text=message, parse_mode=ParseMode.MARKDOWN_V2 22 | ) 23 | -------------------------------------------------------------------------------- /bitdata/provider/__init__.py: -------------------------------------------------------------------------------- 1 | from .bitcoin_rpc import BitcoinRPC 2 | from .blockstream import BlockstreamProvider 3 | -------------------------------------------------------------------------------- /bitdata/provider/bitcoin_rpc.py: -------------------------------------------------------------------------------- 1 | from time import sleep 2 | 3 | from bitcoinrpc.authproxy import AuthServiceProxy, JSONRPCException 4 | from loguru import logger 5 | from tqdm import tqdm 6 | 7 | from ..core.config import BitConfig 8 | 9 | 10 | class BitcoinRPC: 11 | def __init__(self, cfg: BitConfig): 12 | self.cfg = cfg 13 | self.url = f"http://{cfg.RPC_USER}:{cfg.RPC_PASSWORD}@{cfg.RPC_HOST}:{str(cfg.RPC_PORT)}" 14 | self.rpc_conn = AuthServiceProxy(self.url, timeout=120) 15 | 16 | def get_block_by_height(self, height: int): 17 | # Get the block hash from the height 18 | try: 19 | block_hash = self.rpc_conn.getblockhash(height) 20 | logger.info(f"Getting block {block_hash} at height {height}") 21 | except JSONRPCException: 22 | logger.error(f"Block at height {height} not found") 23 | return None 24 | 25 | return self.rpc_conn.getblock(block_hash) 26 | 27 | def get_last_block_height(self): 28 | try: 29 | block_time = self.rpc_conn.getblockcount() 30 | except JSONRPCException: 31 | logger.error("Unable to get block height") 32 | return None 33 | 34 | logger.info(f"Getting last block height {block_time}") 35 | return block_time 36 | 37 | def get_new_address(self): 38 | try: 39 | address = self.rpc_conn.getnewaddress() 40 | except JSONRPCException: 41 | logger.error("Unable to get new address") 42 | return None 43 | 44 | logger.info(f"Getting new address {address}") 45 | return address 46 | 47 | def get_transaction(self, txid: str): 48 | return self.rpc_conn.getrawtransaction(txid, True) 49 | 50 | def address_block_analytics(self, block: dict): 51 | # Create dictionaries to store the number and the amounts associated with each address type 52 | address_types_count = {} 53 | address_types_amount = {} 54 | 55 | for txid in tqdm(block["tx"]): 56 | tx = self.get_transaction(txid) 57 | for output in tx["vout"]: 58 | address_type = output["scriptPubKey"]["type"] 59 | address_types_count[address_type] = ( 60 | address_types_count.get(address_type, 0) + 1 61 | ) 62 | address_types_amount[address_type] = ( 63 | address_types_amount.get(address_type, 0) + output["value"] 64 | ) 65 | 66 | return { 67 | "address_types_count": address_types_count, 68 | "address_types_amount": address_types_amount, 69 | } 70 | -------------------------------------------------------------------------------- /bitdata/provider/blockstream.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | 4 | # Class that use the Blockstream API to get the blockchain data to do analysis 5 | class BlockstreamProvider: 6 | def __init__(self, network="mainnet"): 7 | self.network = network 8 | if network == "testnet": 9 | self.base_url = "https://blockstream.info/testnet/api" 10 | else: 11 | self.base_url = "https://blockstream.info/api" 12 | 13 | def get_block(self, block_height): 14 | return requests.get(f"{self.base_url}/block-height/{block_height}").text 15 | 16 | def get_last_height(self): 17 | return requests.get(f"{self.base_url}/blocks/tip/height").text 18 | 19 | def last_hash(self): 20 | return requests.get(f"{self.base_url}/blocks/tip/hash").text 21 | 22 | def get_last_n_blocks(self, n=10): 23 | result = requests.get(f"{self.base_url}/blocks/:{n}") 24 | print(f"{self.base_url}/blocks/:{n}") 25 | return self.parse_result(result) 26 | 27 | def get_raw_coinbase_transaction(self, block_hash: str = ""): 28 | coinbase_transaction_hash = requests.get( 29 | f"{self.base_url}/block/{block_hash}/txid/0" 30 | ).text 31 | coinbase_transaction_raw = requests.get( 32 | f"{self.base_url}/tx/{coinbase_transaction_hash}/raw" 33 | ).text 34 | return coinbase_transaction_raw 35 | 36 | def parse_result(self, result): 37 | if result.status_code == 200: 38 | return result.json() 39 | else: 40 | return None 41 | 42 | 43 | if __name__ == "__main__": 44 | print("BlockstreamProvider") 45 | # bp = BlockstreamProvider() 46 | 47 | # block = bp.get_block(0) 48 | # print(block) 49 | 50 | # blockchain_info = bp.get_blockchain_info() 51 | # print(blockchain_info) 52 | -------------------------------------------------------------------------------- /bitdata/provider/mempool.py: -------------------------------------------------------------------------------- 1 | import time 2 | import requests 3 | from pydantic import BaseModel 4 | 5 | 6 | class LightningStats(BaseModel): 7 | id: int 8 | added: str 9 | channel_count: int 10 | node_count: int 11 | total_capacity: int 12 | tor_nodes: int 13 | clearnet_nodes: int 14 | unannounced_nodes: int 15 | avg_capacity: int 16 | avg_fee_rate: int 17 | avg_base_fee_mtokens: int 18 | med_capacity: int 19 | med_fee_rate: int 20 | med_base_fee_mtokens: int 21 | clearnet_tor_nodes: int 22 | 23 | 24 | class MempoolProvider: 25 | def __init__(self, network="mainnet") -> None: 26 | self.network = network 27 | if network == "testnet4": 28 | self.base_url = "https://mempool.space/testnet4/api" 29 | else: 30 | self.base_url = "https://mempool.space/api" 31 | 32 | def get_block_hash(self, block_height): 33 | """Fetches block hash by height.""" 34 | response = requests.get(f"{self.base_url}/block-height/{block_height}") 35 | return response.text.strip() 36 | 37 | def get_block(self, block_hash): 38 | """Fetches block data by hash and returns it as a dictionary.""" 39 | response = requests.get(f"{self.base_url}/block/{block_hash}") 40 | return self.parse_result(response) 41 | 42 | def get_last_height(self): 43 | """Fetches the height of the latest block.""" 44 | response = requests.get(f"{self.base_url}/blocks/tip/height") 45 | return int(response.text.strip()) 46 | 47 | def last_hash(self): 48 | """Fetches the hash of the latest block.""" 49 | response = requests.get(f"{self.base_url}/blocks/tip/hash") 50 | return response.text.strip() 51 | 52 | def get_last_n_blocks(self, n=10): 53 | """Fetches the last n blocks.""" 54 | latest_height = self.get_last_height() 55 | start_height = max(0, latest_height - n + 1) 56 | blocks = [] 57 | for height in range(start_height, latest_height + 1): 58 | block_hash = self.get_block_hash(height) 59 | print(block_hash) 60 | time.sleep(1) 61 | block = self.get_block(block_hash) 62 | if block: 63 | blocks.append(block) 64 | return blocks 65 | 66 | def get_raw_coinbase_transaction(self, block_hash: str = ""): 67 | """Fetches the raw coinbase transaction from a given block hash.""" 68 | coinbase_transaction_hash = requests.get( 69 | f"{self.base_url}/block/{block_hash}/txid/0" 70 | ).text 71 | coinbase_transaction_raw = requests.get( 72 | f"{self.base_url}/tx/{coinbase_transaction_hash}/raw" 73 | ).text 74 | return coinbase_transaction_raw 75 | 76 | def get_block_by_hash(self, block_hash): 77 | """Fetches block data by hash.""" 78 | response = requests.get(f"{self.base_url}/block/{block_hash}") 79 | return self.parse_result(response) 80 | 81 | def get_lightning_stats(self): 82 | """Returns network-wide stats such as total number of channels and nodes, total capacity, and average/median fee figures.""" 83 | url = f"{self.base_url}/v1/lightning/statistics/latest" 84 | response = requests.get(url) 85 | result = self.parse_result(response) 86 | if not result: 87 | return None 88 | 89 | stats = result.get("latest", None) 90 | if not stats: 91 | return None 92 | 93 | try: 94 | return LightningStats(**stats) 95 | except Exception as e: 96 | print(e) 97 | return stats 98 | 99 | def parse_result(self, response): 100 | """Parses HTTP response into JSON if status code is 200.""" 101 | if response.status_code == 200: 102 | return response.json() 103 | else: 104 | return None 105 | 106 | 107 | if __name__ == "__main__": 108 | mempool = MempoolProvider() 109 | 110 | # Example usage of the new methods 111 | block_height = 0 112 | print("Block at height", block_height, ":", mempool.get_block(block_height)) 113 | print("Latest block height:", mempool.get_last_height()) 114 | print("Latest block hash:", mempool.last_hash()) 115 | print("Last 10 blocks:", mempool.get_last_n_blocks(10)) 116 | 117 | block_hash = mempool.last_hash() 118 | print("Raw coinbase transaction from block hash", block_hash, ":", mempool.get_raw_coinbase_transaction(block_hash)) 119 | 120 | stats = mempool.get_lightning_stats() 121 | print("Lightning network stats:", stats) 122 | -------------------------------------------------------------------------------- /bitdata/provider/quiknode.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | 4 | class QuickNode: 5 | def __init__(self, api_url="https://docs-demo.btc.quiknode.pro/"): 6 | self.api_url = api_url 7 | 8 | def decode_raw_transaction(self, raw_transaction_hex): 9 | headers = {"Content-Type": "application/json"} 10 | 11 | # JSON data to be sent in the POST request 12 | data = {"method": "decoderawtransaction", "params": [raw_transaction_hex]} 13 | 14 | try: 15 | response = requests.post(api_url, json=data, headers=headers) 16 | if response.status_code == 200: 17 | decoded_transaction = response.json() 18 | return decoded_transaction 19 | else: 20 | print(f"Error: {response.status_code} - {response.text}") 21 | return None 22 | except Exception as e: 23 | print("Error:", e) 24 | return None 25 | -------------------------------------------------------------------------------- /dashboard/On-chain.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | 3 | import matplotlib.pyplot as plt 4 | import pandas as pd 5 | import plotly.express as px 6 | import requests 7 | import streamlit as st 8 | from PIL import Image 9 | 10 | # ------------- Title of the page ------------- 11 | st.set_page_config( 12 | page_title="Bitcoin Blockchain live analysis", page_icon="₿", layout="wide" 13 | ) 14 | # Title and bitcoin logos. a lot of them. 15 | st.title("Analisi in diretta di Bitcoin - BitPolito") 16 | bitcoin_logo = "https://upload.wikimedia.org/wikipedia/commons/thumb/4/46/Bitcoin.svg/1200px-Bitcoin.svg.png" 17 | bitpolito_logo = Image.open("dashboard/bitpolito_logo.png") 18 | col = st.columns(12) 19 | logos = [bitcoin_logo, bitpolito_logo] * 6 20 | for i in range(12): 21 | col[i].image(logos[i], width=50) 22 | 23 | TODO = """ TODO: personalize this 24 | # Configure CSS styles 25 | st.markdown(''' 26 | ''', unsafe_allow_html=True)""" 61 | 62 | # ------------- Bitcoin Nodes ------------- 63 | # create two columns 64 | col1, col2 = st.columns(2) 65 | # ----- on the first column put a map of the world with all the bitcoin nodes 66 | map_data = requests.get( 67 | "https://bitnodes.io/api/v1/snapshots/latest/?field=coordinates" 68 | ) 69 | col1.header("Nodi Bitcoin nel mondo") 70 | map_data = pd.DataFrame(map_data.json()["coordinates"], columns=["lat", "lon"]) 71 | col1.map(map_data, zoom=1, use_container_width=True) 72 | st.write("Fonte: https://bitnodes.io/") 73 | 74 | # ----- on the second column put some statistics about the nodes 75 | col2.header("Statistiche sui nodi") 76 | nodes_data = requests.get("https://bitnodes.io/api/v1/snapshots/latest/") 77 | nodes_data = nodes_data.json() 78 | # numbr of nodes 79 | col2.write(f"Nodi totali: **{nodes_data['total_nodes']}**") 80 | # top cities 81 | cities = {} 82 | for node in nodes_data["nodes"].values(): 83 | if node[-3] not in cities: 84 | cities[node[-3]] = 1 85 | else: 86 | cities[node[-3]] += 1 87 | # sort cities by number of nodes 88 | cities = { 89 | k: v for k, v in sorted(cities.items(), key=lambda item: item[1], reverse=True) 90 | } 91 | del cities[None] 92 | # display top 10 cities in a bullet list 93 | col2.write("Top 10 città per numero di nodi:") 94 | for i, info in enumerate(list(cities)[:10]): 95 | city = info.split("/")[1].replace("_", " ") 96 | continent = info.split("/")[0] 97 | col2.write(f"{i+1}) {city} ({continent}): **{cities[info]} nodi**") 98 | 99 | 100 | # ------------- Date sidebar (for network data) ------------- 101 | st.header("Startistiche sulla rete Bitcoin") 102 | # Define date range dropdown options 103 | date_ranges = { 104 | "All": 365 * 20, 105 | "Last 7 Days": 7, 106 | "Last 30 Days": 30, 107 | "Last 90 Days": 90, 108 | "Last Year": 365, 109 | "Last 5 Years": 365 * 5, 110 | } 111 | # Create a selectbox panel for date filters 112 | date_range = st.selectbox("Date Range", options=list(date_ranges.keys())) 113 | end_date = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0) 114 | start_date = end_date - timedelta(days=date_ranges[date_range]) 115 | 116 | # ------------- Load network data ------------- 117 | def get_blockchaincom_data(url, col): 118 | data = requests.get(url).json() 119 | print(data.keys()) 120 | df = pd.DataFrame(data["values"]).rename(columns={"x": "Date", "y": col}) 121 | df["Date"] = pd.to_datetime(df["Date"], unit="s") 122 | df = df.sort_values(by="Date", ascending=False) 123 | return df 124 | 125 | 126 | def load_heavy_data(): 127 | # Get historical BTC address data from Blockchain.com 128 | addr_url = ( 129 | "https://api.blockchain.info/charts/n-unique-addresses?timespan=all&format=json" 130 | ) 131 | addr_df = get_blockchaincom_data(addr_url, "Addresses") 132 | 133 | # Get historical BTC transaction data from Blockchain.com 134 | tx_url = ( 135 | "https://api.blockchain.info/charts/n-transactions?timespan=all&format=json" 136 | ) 137 | tx_df = get_blockchaincom_data(tx_url, "Transactions") 138 | 139 | # Get historical BTC hash rate data from Blockchain.com 140 | hs_url = "https://api.blockchain.info/charts/hash-rate?timespan=all&format=json" 141 | hs_df = get_blockchaincom_data(hs_url, "Hash") 142 | 143 | # Get latest and second to last block data from Blockchain.com 144 | lastblock = requests.get("https://blockchain.info/latestblock").json() 145 | second_to_last_block = requests.get( 146 | f'https://blockchain.info/block-height/{lastblock["height"]-1}?format=json' 147 | ).json() 148 | 149 | return addr_df, tx_df, hs_df, lastblock, second_to_last_block 150 | 151 | 152 | addr_df, tx_df, hash_df, lastblock, second_to_last_block = load_heavy_data() 153 | addr_df = addr_df.loc[ 154 | (addr_df["Date"] >= pd.Timestamp(start_date)) 155 | & (addr_df["Date"] <= pd.Timestamp(end_date)) 156 | ] 157 | tx_df = tx_df.loc[ 158 | (tx_df["Date"] >= pd.Timestamp(start_date)) 159 | & (tx_df["Date"] <= pd.Timestamp(end_date)) 160 | ] 161 | hash_df = hash_df.loc[ 162 | (hash_df["Date"] >= pd.Timestamp(start_date)) 163 | & (hash_df["Date"] <= pd.Timestamp(end_date)) 164 | ] 165 | 166 | 167 | # ------------- Display network data in charts and metrics ------------- 168 | col1, col2 = st.columns(2) 169 | # Create a line chart of hash rate 170 | with col1: 171 | chart_hash = px.line( 172 | hash_df, 173 | x="Date", 174 | y="Hash", 175 | title="Hash rate totale", 176 | color_discrete_sequence=["#071CD8"], 177 | ) 178 | chart_hash.update_layout(yaxis_title="Hash rate Hash/s") 179 | st.plotly_chart(chart_hash, use_container_width=True) 180 | # Create some other values 181 | with col2: 182 | # metric for current hashrate 183 | current_hash = round(hash_df.iloc[0]["Hash"] / 10**9, 2) 184 | delta = round((hash_df.iloc[0]["Hash"] - hash_df.iloc[1]["Hash"]) / 10**9, 2) 185 | col2.metric( 186 | label="Hash rate attuale", 187 | value=f"{current_hash} TH/s", 188 | delta=f"{delta} TH/s rispetto a 3 giorni fa", 189 | ) 190 | st.divider() 191 | # metric for current fees 192 | st.write("Commissioni (in sat/vB) per includere una transazione in ...") 193 | fees = requests.get("https://blockstream.info/api/fee-estimates").json() 194 | col2_1, col2_2, col2_3 = st.columns(3) 195 | col2_1.metric("1 blocco", f"{fees['1']:0.1f}") 196 | col2_2.metric("6 blocchi", f"{fees['6']:0.1f}") 197 | col2_3.metric("18 blocchi", f"{fees['18']:0.1f}") 198 | st.divider() 199 | # metric for lastest block time 200 | time_since_last_block = datetime.now() - datetime.fromtimestamp(lastblock["time"]) 201 | last_block_minimg_time = datetime.fromtimestamp( 202 | lastblock["time"] 203 | ) - datetime.fromtimestamp(second_to_last_block["blocks"][0]["time"]) 204 | m = "-" if last_block_minimg_time.seconds > 10 * 60 else "" 205 | 206 | col2.metric( 207 | "Ultimo blocco minato ", 208 | f"{time_since_last_block.seconds//60} minuti e {time_since_last_block.seconds%60} seccondi fa", 209 | f"{m}in {last_block_minimg_time.seconds//60} minuti e {last_block_minimg_time.seconds%60} secondi", 210 | ) 211 | st.divider() 212 | 213 | # ------------- Display pools data in charts ------------- 214 | pools = requests.get("https://api.blockchain.info/pools?timespan=7days").json() 215 | # sort json based on values 216 | pools = {k: v for k, v in sorted(pools.items(), key=lambda item: item[1], reverse=True)} 217 | # Extract the top 9 keys and values, and group all the others in a single key 218 | sizes = list(pools.values())[:9] 219 | labels = list(pools.keys())[:9] 220 | sizes.append(sum(list(pools.values())[9:])) 221 | labels.append("Others") 222 | 223 | explode = [0.2 if k == "Unknown" else 0 for k in labels] 224 | colors = [ 225 | "#FFC300", 226 | "#0080FF", 227 | "#FF0000", 228 | "#00BFFF", 229 | "#FF4D4D", 230 | "#0052CC", 231 | "#800000", 232 | "#FF9500", 233 | "#FFEA00", 234 | "#4B0082", 235 | ] 236 | hatches = ["oo", "o", ".", "OO", "xx", "-", "..", "x", "O"] 237 | 238 | fig1, ax1 = plt.subplots(figsize=(2, 2)) 239 | ax1.pie( 240 | sizes, 241 | autopct="%1.1f%%", 242 | pctdistance=1.25, 243 | explode=explode, 244 | colors=colors, 245 | hatch=hatches, 246 | textprops={"fontsize": 6}, 247 | ) 248 | ax1.legend(labels, loc="center left", bbox_to_anchor=(1.25, 0.5), fontsize=6) 249 | st.pyplot(fig1, use_container_width=False) 250 | 251 | # ------------- Display address and transaction data in graphs ------------- 252 | col1, col2 = st.columns(2) 253 | # Create a line chart of daily addresses 254 | with col1: 255 | chart_txn = px.line( 256 | tx_df, 257 | x="Date", 258 | y="Transactions", 259 | title="Transazioni giornaliere", 260 | color_discrete_sequence=["#F7931A"], 261 | ) 262 | chart_txn.update_layout(yaxis_title="Transactions") 263 | st.plotly_chart(chart_txn, use_container_width=True) 264 | # Create a line chart of daily transactions 265 | with col2: 266 | chart_addr = px.line( 267 | addr_df, 268 | x="Date", 269 | y="Addresses", 270 | title="Indirizzi attivi giornalieri", 271 | color_discrete_sequence=["#F7931A"], 272 | ) 273 | chart_addr.update_layout(yaxis_title="Active Addresses") 274 | st.plotly_chart(chart_addr, use_container_width=True) 275 | 276 | st.write("Fonte: https://www.blockchain.info") 277 | st.write("Fonte: https://blockstream.info") 278 | 279 | preference = st.sidebar.radio("Cosa preferisci?", ("-seleziona-", "Bitcoin", "Fiat")) 280 | 281 | if preference == "Bitcoin": 282 | st.balloons() 283 | elif preference == "Fiat": 284 | st.snow() 285 | -------------------------------------------------------------------------------- /dashboard/bitpolito_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitPolito/bitcoin-data-analysis/404e705fe2bc8e6c900e75a3ac28bd27588b2bc4/dashboard/bitpolito_logo.png -------------------------------------------------------------------------------- /dashboard/lightning.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitPolito/bitcoin-data-analysis/404e705fe2bc8e6c900e75a3ac28bd27588b2bc4/dashboard/lightning.gif -------------------------------------------------------------------------------- /dashboard/pages/Lightning_Network.py: -------------------------------------------------------------------------------- 1 | import base64 2 | 3 | import requests 4 | import streamlit as st 5 | from PIL import Image 6 | 7 | from bitdata.provider.mempool import MempoolProvider 8 | 9 | # ------------- Title of the page ------------- 10 | st.set_page_config( 11 | page_title="Bitcoin Blockchain live analysis", page_icon="₿", layout="wide" 12 | ) 13 | # Title and bitcoin logos. a lot of them. 14 | st.title("Analisi in diretta di Lightning Network - BitPolito") 15 | bitcoin_logo = "https://upload.wikimedia.org/wikipedia/commons/thumb/4/46/Bitcoin.svg/1200px-Bitcoin.svg.png" 16 | bitpolito_logo = Image.open("dashboard/bitpolito_logo.png") 17 | col = st.columns(12) 18 | logos = [bitcoin_logo, bitpolito_logo] * 6 19 | for i in range(12): 20 | col[i].image(logos[i], width=50) 21 | 22 | 23 | # Lightning Network Stats 24 | ln_stats = MempoolProvider().get_lightning_stats() 25 | # ln_sats: id=37293 added='2023-06-02T00:00:00.000Z' channel_count=70378 node_count=15700 total_capacity=536810389159 tor_nodes=11095 clearnet_nodes=2167 unannounced_nodes=1000 avg_capacity=7627531 avg_fee_rate=547 avg_base_fee_mtokens=850 med_capacity=2000000 med_fee_rate=40 med_base_fee_mtokens=125 clearnet_tor_nodes=1438 26 | 27 | st.metric(label="Total number of nodes", value=ln_stats.node_count) 28 | st.metric(label="Total number of channels", value=ln_stats.channel_count) 29 | st.metric(label="Total capacity", value=ln_stats.total_capacity) 30 | st.metric(label="Tor nodes", value=ln_stats.tor_nodes) 31 | st.metric(label="Clearnet nodes", value=ln_stats.clearnet_nodes) 32 | st.metric(label="Unannounced nodes", value=ln_stats.unannounced_nodes) 33 | st.metric(label="Average capacity", value=ln_stats.avg_capacity) 34 | st.metric(label="Average fee rate", value=ln_stats.avg_fee_rate) 35 | 36 | 37 | st.header("Numero totale di nodi e canali") 38 | st.subheader("Numero totale di nodi") 39 | st.write("Il numero totale di nodi è pari a: ") 40 | st.write("Il numero totale di canali è pari a: ") 41 | 42 | # Lightning Graph 43 | 44 | st.header("Lightning Network Graph") 45 | # Add description 46 | st.expander( 47 | """ 48 | Li 49 | """ 50 | ) 51 | 52 | # Add iframe graph 53 | width = 800 54 | height = 600 55 | lngraph = f'