diff --git a/backend/moonstream/api.py b/backend/moonstream/api.py index 9160b086..412e3834 100644 --- a/backend/moonstream/api.py +++ b/backend/moonstream/api.py @@ -9,6 +9,7 @@ from fastapi.middleware.cors import CORSMiddleware from . import data from .routes.subscriptions import app as subscriptions_api from .routes.users import app as users_api +from .routes.txinfo import app as txinfo_api from .settings import ORIGINS from .version import MOONSTREAM_VERSION @@ -38,3 +39,4 @@ async def version_handler() -> data.VersionResponse: app.mount("/subscriptions", subscriptions_api) app.mount("/users", users_api) +app.mount("/txinfo", txinfo_api) diff --git a/backend/moonstream/data.py b/backend/moonstream/data.py index 01944f84..f11f338c 100644 --- a/backend/moonstream/data.py +++ b/backend/moonstream/data.py @@ -88,3 +88,24 @@ class EVMEventSignature(BaseModel): class ContractABI(BaseModel): functions: List[EVMFunctionSignature] events: List[EVMEventSignature] + + +class EthereumTransaction(BaseModel): + gas: int + gasPrice: int + value: int + from_address: str = Field(alias="from") + to_address: Optional[str] = Field(default=None, alias="to") + hash: Optional[str] = None + input: Optional[str] = None + + +class TxinfoEthereumBlockchainRequest(BaseModel): + tx: EthereumTransaction + + +class TxinfoEthereumBlockchainResponse(BaseModel): + tx: EthereumTransaction + abi: Optional[ContractABI] = None + errors: List[str] = Field(default_factory=list) + diff --git a/backend/moonstream/routes/txinfo.py b/backend/moonstream/routes/txinfo.py new file mode 100644 index 00000000..033ce902 --- /dev/null +++ b/backend/moonstream/routes/txinfo.py @@ -0,0 +1,80 @@ +""" +Moonstream's /txinfo endpoints. + +These endpoints enrich raw blockchain transactions (as well as pending transactions, hypothetical +transactions, etc.) with side information and return objects that are better suited for displaying to +end users. +""" +import logging +from typing import Any, Dict + +from fastapi import ( + FastAPI, + Depends, + HTTPException, + Request, +) +from fastapi.middleware.cors import CORSMiddleware +from moonstreamdb.db import yield_db_session +from sqlalchemy.orm import Session + +from ..abi_decoder import decode_abi +from ..data import TxinfoEthereumBlockchainRequest, TxinfoEthereumBlockchainResponse +from ..middleware import BroodAuthMiddleware +from ..settings import ( + MOONSTREAM_APPLICATION_ID, + DOCS_TARGET_PATH, + ORIGINS, + DOCS_PATHS, + bugout_client as bc, +) +from ..version import MOONSTREAM_VERSION + +logger = logging.getLogger(__name__) + +tags_metadata = [ + {"name": "users", "description": "Operations with users."}, + {"name": "tokens", "description": "Operations with user tokens."}, +] + +app = FastAPI( + title=f"Moonstream users API.", + description="User, token and password handlers.", + version=MOONSTREAM_VERSION, + openapi_tags=tags_metadata, + openapi_url="/openapi.json", + docs_url=None, + redoc_url=f"/{DOCS_TARGET_PATH}", +) + +app.add_middleware( + CORSMiddleware, + allow_origins=ORIGINS, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +whitelist_paths: Dict[str, str] = {} +whitelist_paths.update(DOCS_PATHS) +app.add_middleware(BroodAuthMiddleware, whitelist=whitelist_paths) + + +@app.post( + "/ethereum_blockchain", + tags=["txinfo"], + response_model=TxinfoEthereumBlockchainResponse, +) +async def txinfo_ethereum_blockchain_handler( + txinfo_request: TxinfoEthereumBlockchainRequest, + db_session: Session = Depends(yield_db_session), +) -> TxinfoEthereumBlockchainResponse: + response = TxinfoEthereumBlockchainResponse(tx=txinfo_request.tx) + if txinfo_request.tx.input is not None: + try: + response.abi = decode_abi(txinfo_request.tx.input, db_session) + except Exception as err: + logger.error(r"Could not decode ABI:") + logger.error(err) + response.errors.append("Could not decode ABI from the given input") + return response diff --git a/backend/scripts/txinfo_ethereum_blockchain.bash b/backend/scripts/txinfo_ethereum_blockchain.bash new file mode 100755 index 00000000..8a274a5f --- /dev/null +++ b/backend/scripts/txinfo_ethereum_blockchain.bash @@ -0,0 +1,52 @@ +#!/usr/bin/env bash + +TIMESTAMP="$(date +%s)" +SCRIPT_DIR=$(realpath $(dirname $0)) + +API_URL="${MOONSTREAM_DEV_API_URL:-http://localhost:7481}" + +MOONSTREAM_USERNAME="devuser_$TIMESTAMP" +MOONSTREAM_PASSWORD="peppercat" +MOONSTREAM_EMAIL="devuser_$TIMESTAMP@example.com" + +OUTPUT_DIR=$(mktemp -d) +echo "Writing responses to directory: $OUTPUT_DIR" + +# Create a new user +curl -X POST \ + -H "Content-Type: multipart/form-data" \ + "$API_URL/users/" \ + -F "username=$MOONSTREAM_USERNAME" \ + -F "password=$MOONSTREAM_PASSWORD" \ + -F "email=$MOONSTREAM_EMAIL" \ + -o $OUTPUT_DIR/user.json + +# Create a token for this user +curl -X POST \ + -H "Content-Type: multipart/form-data" \ + "$API_URL/users/token" \ + -F "username=$MOONSTREAM_USERNAME" \ + -F "password=$MOONSTREAM_PASSWORD" \ + -o $OUTPUT_DIR/token.json + +API_TOKEN=$(jq -r '.id' $OUTPUT_DIR/token.json) + +set -e + +ETHEREUM_TXINFO_REQUEST_BODY_JSON=$(jq -r . $SCRIPT_DIR/txinfo_ethereum_blockchain_request.json) +curl -f -X POST \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $API_TOKEN" \ + "$API_URL/txinfo/ethereum_blockchain" \ + -d "$ETHEREUM_TXINFO_REQUEST_BODY_JSON" \ + -o $OUTPUT_DIR/txinfo_response.json + +echo "Response:" +jq . $OUTPUT_DIR/txinfo_response.json + +if [ "$DEBUG" != true ] +then + echo "Deleting output directory: $OUTPUT_DIR" + echo "Please set DEBUG=true if you would prefer to retain this directory in the future" + rm -r $OUTPUT_DIR +fi \ No newline at end of file diff --git a/backend/scripts/txinfo_ethereum_blockchain_request.json b/backend/scripts/txinfo_ethereum_blockchain_request.json new file mode 100644 index 00000000..1a78035e --- /dev/null +++ b/backend/scripts/txinfo_ethereum_blockchain_request.json @@ -0,0 +1,11 @@ +{ + "tx": { + "to": null, + "from": "0x2E337E0Fb68F5e51ce9295E80BCd02273d7420c4", + "gas": 2265656, + "gasPrice": 1000000000, + "hash": "0x5f0b6e212e55c7120f36fe6f88d46eb001c848064fd099116b42805bb3564ae6", + "value": 0, + "input": "0x606061026b61014039602061026b60c03960c05160a01c1561002057600080fd5b61014051600055610160516001556001546101805181818301101561004457600080fd5b80820190509050600255600254421061005c57600080fd5b61025356600436101561000d576101ec565b600035601c52600051631998aeef8114156100855760015442101561003157600080fd5b600254421061003f57600080fd5b600454341161004d57600080fd5b600660035460e05260c052604060c020805460045481818301101561007157600080fd5b808201905090508155503360035534600455005b341561009057600080fd5b633ccfd60b8114156100db5760063360e05260c052604060c0205461014052600060063360e05260c052604060c02055600060006000600061014051336000f16100d957600080fd5b005b63fe67a54b811415610124576002544210156100f657600080fd5b6005541561010357600080fd5b600160055560006000600060006004546000546000f161012257600080fd5b005b6338af3eed81141561013c5760005460005260206000f35b634f245ef78114156101545760015460005260206000f35b632a24f46c81141561016c5760025460005260206000f35b6391f901578114156101845760035460005260206000f35b63d57bde7981141561019c5760045460005260206000f35b6312fa6feb8114156101b45760055460005260206000f35b6326b387bb8114156101ea5760043560a01c156101d057600080fd5b600660043560e05260c052604060c0205460005260206000f35b505b60006000fd5b61006161025303610061600039610061610253036000f30000000000000000000000002e337e0fb68f5e51ce9295e80bcd02273d7420c40000000000000000000000000000000000000000000000000000000060d2b04a00000000000000000000000000000000000000000000000000000000616b46ca" + } +}