feat: get raw transaction data from bitcoin core

This commit is contained in:
fusion44 2022-11-01 20:46:08 +01:00
parent 2053f0ea3b
commit d9ea422e91
No known key found for this signature in database
4 changed files with 117 additions and 4 deletions

View file

@ -1,6 +1,6 @@
from argparse import ArgumentError
from enum import Enum
from typing import List, Optional
from typing import List, Optional, Union
from fastapi import Query
from pydantic.main import BaseModel
@ -64,6 +64,55 @@ class BtcLocalAddress(BaseModel):
)
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")

View file

@ -3,12 +3,19 @@ from fastapi.params import Depends, Query
from app.auth.auth_bearer import JWTBearer
from app.bitcoind.docs import blocks_sub_doc, estimate_fee_mode_desc
from app.bitcoind.models import BlockchainInfo, BtcInfo, FeeEstimationMode, NetworkInfo
from app.bitcoind.models import (
BlockchainInfo,
BtcInfo,
FeeEstimationMode,
NetworkInfo,
RawTransaction,
)
from app.bitcoind.service import (
estimate_fee,
get_blockchain_info,
get_btc_info,
get_network_info,
get_raw_transaction,
handle_block_sub,
)
from app.bitcoind.utils import bitcoin_rpc
@ -106,6 +113,27 @@ async def getnetworkinfo():
return info
@router.get(
"/get-raw-transaction",
name=f"{_PREFIX}.get-raw-transaction",
summary="Get information about a raw transaction",
description="See documentation on [bitcoincore.org](https://bitcoincore.org/en/doc/22.0.0/rpc/rawtransactions/getrawtransaction/)",
response_description="A JSON String with relevant information.",
dependencies=[Depends(JWTBearer())],
response_model=RawTransaction,
responses={
400: {"description": "Invalid transaction id"},
404: {"description": "No such mempool or blockchain transaction."},
},
)
async def get_raw_transaction_path(
txid: str = Query(
..., min_length=64, max_length=64, description="The transaction id"
)
):
return await get_raw_transaction(txid)
@router.get(
"/block-sub",
name=f"{_PREFIX}.block-sub",

View file

@ -17,6 +17,7 @@ from app.bitcoind.models import (
BtcInfo,
FeeEstimationMode,
NetworkInfo,
RawTransaction,
)
from app.bitcoind.utils import bitcoin_config, bitcoin_rpc_async
@ -91,6 +92,21 @@ async def get_network_info() -> NetworkInfo:
return NetworkInfo.from_rpc(result["result"])
async def get_raw_transaction(txid: str) -> RawTransaction:
result = await bitcoin_rpc_async("getrawtransaction", [txid, 1])
if result["error"] == None:
return RawTransaction.from_rpc(result["result"])
if "No such mempool or blockchain transaction." in result["error"]:
raise HTTPException(status.HTTP_404_NOT_FOUND, detail=result["error"])
if "must be of length 64" in result["error"]:
raise HTTPException(status.HTTP_404_NOT_FOUND, detail=result["error"])
raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR, detail=result["error"])
async def get_btc_info() -> BtcInfo:
binfo = await get_blockchain_info()
ninfo = await get_network_info()

View file

@ -1,3 +1,4 @@
import itertools
import json
from types import coroutine
@ -61,17 +62,22 @@ def bitcoin_rpc(method: str, params: list = []) -> requests.Response:
return requests.post(bitcoin_config.rpc_url, auth=auth, headers=headers, data=data)
# https://github.com/python/cpython/blob/3.10/Lib/asyncio/tasks.py#L31
_generate_rpc_id = itertools.count(1).__next__
async def bitcoin_rpc_async(method: str, params: list = []) -> coroutine:
auth = aiohttp.BasicAuth(bitcoin_config.username, bitcoin_config.pw)
headers = {"Content-type": "text/plain"}
headers = {"Content-type": "text/json"}
data = (
'{"jsonrpc": "2.0", "method": "'
+ method
+ '", "id":"0", "params":'
+ f'", "id":{_generate_rpc_id()}, "params":'
+ json.dumps(params)
+ "}"
)
# TODO: Refactor this to use Exceptions
async with aiohttp.ClientSession(auth=auth, headers=headers) as session:
async with session.post(bitcoin_config.rpc_url, data=data) as resp:
if resp.status == status.HTTP_200_OK:
@ -87,6 +93,20 @@ async def bitcoin_rpc_async(method: str, params: list = []) -> coroutine:
"status": status.HTTP_403_FORBIDDEN,
}
else:
e = await resp.json()
m = e["error"]["message"]
if e["error"]:
if "No such mempool or blockchain transaction." in m:
return {
"error": "No such mempool or blockchain transaction.",
"status": status.HTTP_404_NOT_FOUND,
}
if "parameter 1 must be of length 64" in m:
return {
"error": m,
"status": status.HTTP_400_BAD_REQUEST,
}
return {
"error": f"Unknown answer from Bitcoin Core. Reason: {resp.reason}",
"status": resp.status,