from argparse import ArgumentError from enum import Enum from typing import List, Optional, Union from fastapi import Query from pydantic.main import BaseModel class FeeEstimationMode(str, Enum): CONSERVATIVE = "conservative" ECONOMICAL = "economical" class BlockRpcFunc(str, Enum): HASHBLOCK = "hashblock" RAWBLOCK = "rawblock" @classmethod def from_string(cls, func: str): if func == "hashblock": return cls.HASHBLOCK elif func == "rawblock": return cls.RAWBLOCK else: raise ArgumentError( None, "Function name must either be 'hashblock' or 'rawblock'." f" Actual: ${func}", ) class BtcNetwork(BaseModel): name: str = Query(..., description="Which network is in use (ipv4, ipv6 or onion)") limited: bool = Query(..., description="Is the network limited using - onlynet?") reachable: bool = Query(..., description="Is the network reachable?") proxy: Optional[str] = Query( "", description=( "host:port of the proxy that is used for this network, or empty if none" ), ) proxy_randomize_credentials: bool = Query( ..., description="Whether randomized credentials are used" ) @classmethod def from_rpc(cls, r): return cls( name=r["name"], limited=r["limited"], reachable=r["reachable"], proxy=r["proxy"], proxy_randomize_credentials=r["proxy_randomize_credentials"], ) class BtcLocalAddress(BaseModel): address: str = Query(..., description="Network address") port: int = Query(..., description="Network port") score: int = Query(..., description="Relative score") @classmethod def from_rpc(cls, local_address): return BtcLocalAddress( address=local_address["address"], port=local_address["port"], score=local_address["score"], ) class RawTransaction(BaseModel): in_active_chain: Union[None, bool] = Query( None, description=( "Whether specified block is in the active chain or not (only present with " "explicit 'blockhash' argument)" ), ) txid: str = Query(..., description="The transaction id (same as provided)") hash: str = Query( ..., description="The transaction hash (differs from txid for witness transactions)", ) size: int = Query(..., description="The serialized transaction size") vsize: int = Query( ..., description=( "The virtual transaction size (differs from size for witness transactions)" ), ) weight: int = Query( ..., description="The transaction's weight (between vsize*4 - 3 and vsize*4)" ) version: int = Query(..., description="The version") locktime: int = Query(..., description="The lock time") vin: List[dict] = Query(..., description="The transaction's inputs") vout: List[dict] = Query(..., description="The transaction's outputs") blockhash: str = Query(..., description="The block hash") confirmations: int = Query(..., description="The number of confirmations") blocktime: int = Query( ..., description="The block time in seconds since epoch (Jan 1 1970 GMT)" ) @classmethod def from_rpc(cls, tx): return cls( in_active_chain=( tx["in_active_chain"] if "in_active_chain" in tx.keys() else None ), txid=tx["txid"] if "txid" in tx.keys() else "", hash=tx["hash"] if "hash" in tx.keys() else "", size=tx["size"] if "size" in tx.keys() else 0, vsize=tx["vsize"] if "vsize" in tx.keys() else 0, weight=tx["weight"] if "weight" in tx.keys() else 0, version=tx["version"] if "version" in tx.keys() else 0, locktime=tx["locktime"] if "locktime" in tx.keys() else 0, vin=tx["vin"] if "vin" in tx.keys() else [], vout=tx["vout"] if "vout" in tx.keys() else [], blockhash=tx["blockhash"] if "blockhash" in tx.keys() else "", confirmations=tx["confirmations"] if "confirmations" in tx.keys() else 0, blocktime=tx["blocktime"] if "blocktime" in tx.keys() else 0, ) # getnetworkinfo class NetworkInfo(BaseModel): version: int = Query(..., description="The bitcoin core server version") subversion: str = Query(..., description="The server subversion string") protocol_version: int = Query(..., description="The protocol version") local_services: str = Query( None, description="The services we offer to the network, hex formatted" ) local_services_names: List[str] = Query( [], description="The services we offer to the network, in human-readable form" ) local_relay: bool = Query( ..., description="True if transaction relay is requested from peers" ) time_offset: int = Query(..., description="The time offset") connections: int = Query(..., description="The total number of connections") connections_in: int = Query(..., description="The number of inbound connections") connections_out: int = Query(..., description="The number of outbound connections") network_active: bool = Query(..., description="Whether p2p networking is enabled") networks: List[BtcNetwork] = Query(..., description="Information per network") relay_fee: float = Query( ..., description="Minimum relay fee for transactions in BTC/kB" ) incremental_fee: float = Query( ..., description=( "Minimum fee increment for mempool limiting or BIP 125 " "replacement in BTC/kB" ), ) local_addresses: List[BtcLocalAddress] = Query( [], description="List of local addresses" ) warnings: str = Query("", description="Any network and blockchain warnings") @classmethod def from_rpc(cls, r): return cls( version=r["version"], subversion=r["subversion"], protocol_version=r["protocolversion"], local_services=r["localservices"], local_services_names=[name for name in r["localservicesnames"]], local_relay=r["localrelay"], time_offset=r["timeoffset"], connections=r["connections"], connections_in=r["connections_in"], connections_out=r["connections_out"], network_active=r["networkactive"], networks=[BtcNetwork.from_rpc(n) for n in r["networks"]], relay_fee=r["relayfee"], incremental_fee=r["incrementalfee"], local_addresses=[BtcLocalAddress.from_rpc(n) for n in r["localaddresses"]], warnings="" if "warnings" not in r else str(r["warnings"]), ) class Bip9Statistics(BaseModel): period: int = Query( ..., description="The length in blocks of the BIP9 signalling period" ) threshold: int = Query( ..., description=( "The number of blocks with the version bit set required to activate " "the feature" ), ) elapsed: int = Query( ..., description=( "The number of blocks elapsed since the beginning of the current period" ), ) count: int = Query( ..., description=( "The number of blocks with the version bit set in the current period" ), ) possible: bool = Query( ..., description=( "False if there are not enough blocks left in this period to " "pass activation threshold" ), ) @classmethod def from_rpc(cls, r): return cls( period=r["period"], threshold=r["threshold"], elapsed=r["elapsed"], count=r["count"], possible=r["possible"], ) class Bip9Data(BaseModel): status: str = Query( ..., description='One of "defined", "started", "locked_in", "active", "failed"', ) bit: int | None = Query( None, description=( "the bit(0-28) in the block version field used to signal this " "softfork(only for `started` status)" ), ) start_time: int = Query( ..., description=( "The minimum median time past of a block at which the bit gains its meaning" ), ) timeout: int = Query( ..., description=( "The median time past of a block at which the deployment is " "considered failed if not yet locked in" ), ) since: int = Query( ..., description="Height of the first block to which the status applies" ) min_activation_height: int = Query( ..., description="Minimum height of blocks for which the rules may be enforced" ) statistics: Bip9Statistics | None = Query( None, description=( "numeric statistics about BIP9 signalling for a " "softfork(only for `started` status)" ), ) height: int | None = Query( None, description=( "Height of the first block which the rules are or will be " "enforced(only for `buried` type, or `bip9` type with `active` status)" ), ) active: bool | None = Query( None, description="True if the rules are enforced for the mempool and the next block", ) @classmethod def from_rpc(cls, r): return cls( status=r["status"], bit=r["bit"] if "bit" in r else None, start_time=r["start_time"], timeout=r["timeout"], since=r["since"], min_activation_height=r["min_activation_height"], statistics=( Bip9Statistics.from_rpc(r["statistics"]) if "statistics" in r else None ), height=r["height"] if "height" in r else None, active=r["active"] if "active" in r else None, ) class SoftFork(BaseModel): name: str = Query(..., description="Name of the softfork") type: str = Query(..., description='One of "buried", "bip9"') active: bool = Query( ..., description=( "True **if** the rules are enforced for the mempool and the next block" ), ) bip9: Bip9Data | None = Query( None, description='Status of bip9 softforks(only for "bip9" type)' ) height: int | None = Query( None, description=( "Height of the first block which the rules are or will be enforced " "(only for `buried` type, or `bip9` type with `active` status)" ), ) @classmethod def from_rpc(cls, name: str, r: dict): return cls( name=name, type=r["type"], active=r["active"], bip9=Bip9Data.from_rpc(r["bip9"]) if "bip9" in r else None, height=r["height"] if "height" in r else None, ) class BlockchainInfo(BaseModel): chain: str = Query(..., description="Current network name(main, test, regtest)") blocks: int = Query( ..., description=( "The height of the most-work fully-validated chain. " "The genesis block has height 0" ), ) headers: int = Query( ..., description="The current number of headers we have validated" ) best_block_hash: str = Query( ..., description="The hash of the currently best block" ) difficulty: float = Query(..., description="The current difficulty") mediantime: int = Query(..., description="Median time for the current best block") verification_progress: float = Query( ..., description="Estimate of verification progress[0..1]" ) initial_block_download: bool = Query( ..., description="Estimate of whether this node is in Initial Block Download mode", ) chainwork: str = Query( ..., description="total amount of work in active chain, in hexadecimal" ) size_on_disk: int = Query( ..., description="The estimated size of the block and undo files on disk" ) pruned: bool = Query(..., description="If the blocks are subject to pruning") prune_height: int | None = Query( None, description=( "Lowest-height complete block stored(only present if pruning is enabled)" ), ) automatic_pruning: bool | None = Query( None, description=( "Whether automatic pruning is enabled(only present if pruning is enabled)" ), ) prune_target_size: int | None = Query( None, description=( "The target size used by pruning(only present if automatic pruning is " "enabled)" ), ) warnings: str = Query("", description="Any network and blockchain warnings") softforks: List[SoftFork] = Query(..., description="Status of softforks") @classmethod def from_rpc(cls, r): # get softfork information if available softforks = [] if "softforks" in r: for name in r["softforks"]: softforks.append(SoftFork.from_rpc(name, r["softforks"][name])) return cls( chain=r["chain"], blocks=r["blocks"], headers=r["headers"], best_block_hash=r["bestblockhash"], difficulty=r["difficulty"], mediantime=r["mediantime"], verification_progress=r["verificationprogress"], initial_block_download=r["initialblockdownload"], chainwork=r["chainwork"], size_on_disk=r["size_on_disk"], pruned=r["pruned"], prune_height=None if "pruneheight" not in r else r["pruneheight"], automatic_pruning=( None if "automatic_pruning" not in r else bool(r["automatic_pruning"]) ), prune_target_size=( None if "prune_target_size" not in r else int(r["prune_target_size"]) ), warnings="" if "warnings" not in r else str(r["warnings"]), softforks=softforks, ) class BtcInfo(BaseModel): # Info regarding bitcoind blocks: int = Query( ..., description=( "The height of the most-work fully-validated chain. " "The genesis block has height 0" ), ) headers: int = Query( ..., description="The current number of headers we have validated" ) verification_progress: float = Query( ..., description="Estimate of verification progress[0..1]" ) difficulty: float = Query(..., description="The current difficulty") size_on_disk: int = Query( ..., description="The estimated size of the block and undo files on disk" ) networks: List[BtcNetwork] = Query( [], description="Which networks are in use (ipv4, ipv6 or onion)" ) version: int = Query(..., description="The bitcoin core server version") subversion: str = Query(..., description="The server subversion string") connections_in: int = Query(..., description="The number of inbound connections") connections_out: int = Query(..., description="The number of outbound connections") @classmethod def from_rpc(cls, binfo: BlockchainInfo, ninfo: NetworkInfo): return cls( blocks=binfo.blocks, headers=binfo.headers, verification_progress=binfo.verification_progress, difficulty=binfo.difficulty, size_on_disk=binfo.size_on_disk, networks=ninfo.networks, version=ninfo.version, subversion=ninfo.subversion, connections_in=ninfo.connections_in, connections_out=ninfo.connections_out, )