Skip to content

Commit

Permalink
Merge pull request #13 from Jwyman328/feature/privacy
Browse files Browse the repository at this point in the history
Feature/privacy
  • Loading branch information
Jwyman328 authored Dec 15, 2024
2 parents fba496b + 514d95e commit 548774e
Show file tree
Hide file tree
Showing 56 changed files with 7,441 additions and 177 deletions.
727 changes: 727 additions & 0 deletions NOTES.md

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@

# TODOs backend
- add mypy
- add testing system that tests the database, allowing for more integration level tests, especially in services, I shouldn't have to mock out db calls in the services.
- using sqllite should be easy to setup and teardown the db each run.
- add a pyproject.toml to manage ruff line length stuff so that ruff formatting is inline with the lsp?
- Also set the default custom fee rate to the current rate

Expand Down
1 change: 1 addition & 0 deletions backend/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ chardet
charset-normalizer==2.1.0
hwi===3.0.0
cryptography==42.0.8
bitcoinlib==0.6.15
1 change: 1 addition & 0 deletions backend/src/api/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
from src.api.fees import get_fees
from src.api.electrum import electrum_request, parse_electrum_url, ElectrumMethod
110 changes: 110 additions & 0 deletions backend/src/api/electrum.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
from enum import Enum
from typing import List, Optional, Literal
from dataclasses import dataclass
from bitcoinlib.transactions import Transaction
import structlog
import json
import socket
import asyncio

LOGGER = structlog.get_logger()


def parse_electrum_url(electrum_url: str) -> tuple[Optional[str], Optional[str]]:
try:
url, port = electrum_url.split(":")
if url == "" or port == "":
return None, None
return url, port
except ValueError:
return None, None


class ElectrumMethod(Enum):
GET_TRANSACTIONS = "blockchain.transaction.get"


@dataclass
class GetTransactionsRequestParams:
txid: str
verbose: bool

def create_params_list(self) -> List[str | bool]:
return [self.txid, self.verbose]


GetTransactionsResponse = Transaction

ALL_UTXOS_REQUEST_PARAMS = GetTransactionsRequestParams
ElectrumRawResponses = dict[str, str]
ElectrumDataResponses = GetTransactionsResponse


@dataclass
class ElectrumResponse:
status: Literal["success", "error"]
data: Optional[ElectrumDataResponses]


async def electrum_request(
url: str,
port: int,
electrum_method: ElectrumMethod,
params: Optional[ALL_UTXOS_REQUEST_PARAMS],
request_id: Optional[int] = 1,
) -> ElectrumResponse:
writer = None
try:
# Use asyncio to manage the socket connection
reader, writer = await asyncio.open_connection(url, port)

request = json.dumps(
{
"jsonrpc": "2.0",
"id": request_id,
"method": electrum_method.value,
"params": params.create_params_list() if params else [],
}
)

LOGGER.info(f"Sending electrum request: {request}")
writer.write((request + "\n").encode("utf-8"))
await writer.drain()

# Read the response asynchronously
response = b""
while True:
part = await reader.read(4096)
response += part
if len(part) < 4096:
break

# Decode and parse the JSON response
raw_response_data = json.loads(response.decode("utf-8").strip())

# Assuming handle_raw_electrum_response is an existing function
response_data = handle_raw_electrum_response(
electrum_method, raw_response_data)
return ElectrumResponse(status="success", data=response_data)
except socket.error as e:
LOGGER.error(f"Socket error: {e}")
return ElectrumResponse(status="error", data=None)
except json.JSONDecodeError as e:
LOGGER.error(f"JSON decode error: {e}")
return ElectrumResponse(status="error", data=None)
except Exception as e:
LOGGER.error(f"An error occurred: {e}")
return ElectrumResponse(status="error", data=None)
finally:
# Close the writer/reader
if writer:
writer.close()
await writer.wait_closed()


def handle_raw_electrum_response(
electrum_method: ElectrumMethod, raw_response: dict
) -> ElectrumDataResponses:
if electrum_method == ElectrumMethod.GET_TRANSACTIONS:
transaction = Transaction.parse(raw_response["result"], strict=True)
return transaction
14 changes: 12 additions & 2 deletions backend/src/app.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from flask import Flask, request
from flask_cors import CORS
from src.database import DB
from src.database import DB, populate_labels, populate_privacy_metrics

# initialize structlog
from src.utils import logging # noqa: F401, E261
Expand All @@ -19,10 +19,12 @@ def create_app(cls) -> Flask:
from src.controllers import (
balance_page,
utxo_page,
transactions_page,
fees_api,
wallet_api,
health_check_api,
hardware_wallet_api,
privacy_metrics_api,
)
from src.containers.service_container import ServiceContainer

Expand All @@ -45,10 +47,12 @@ def create_app(cls) -> Flask:
cls.app.container = container
cls.app.register_blueprint(balance_page)
cls.app.register_blueprint(utxo_page)
cls.app.register_blueprint(transactions_page)
cls.app.register_blueprint(fees_api)
cls.app.register_blueprint(wallet_api)
cls.app.register_blueprint(health_check_api)
cls.app.register_blueprint(hardware_wallet_api)
cls.app.register_blueprint(privacy_metrics_api)

return cls.app

Expand Down Expand Up @@ -91,7 +95,12 @@ def setup_database(app):
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
DB.init_app(app)
with app.app_context():
# Drop all existing tables
DB.drop_all() # Clear the database, incase anything was left over.

DB.create_all()
populate_labels()
populate_privacy_metrics()


# for some reason the frontend doesn't run the executable with app.y being __main__
Expand All @@ -102,7 +111,8 @@ def setup_database(app):

if is_testing is False:
# hwi will fail on macos unless it is run in a single thread, threrefore set threaded to False
app.run(host="127.0.0.1", port=5011, debug=is_development, threaded=False)
app.run(host="127.0.0.1", port=5011,
debug=is_development, threaded=False)
else:
# this will run when the app is run from the generated executable
# which is done in the production app.
Expand Down
7 changes: 6 additions & 1 deletion backend/src/containers/service_container.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
from dependency_injector import containers, providers

from src.services.privacy_metrics.privacy_metrics import PrivacyMetricsService


class ServiceContainer(containers.DeclarativeContainer):
from src.services import WalletService, FeeService, HardwareWalletService

wiring_config = containers.WiringConfiguration(packages=["..controllers", "..services"])
wiring_config = containers.WiringConfiguration(
packages=["..controllers", "..services"]
)

wallet_service = providers.Factory(WalletService)
hardware_wallet_service = providers.Singleton(HardwareWalletService)
fee_service = providers.Factory(FeeService)
privacy_metrics_service = providers.Factory(PrivacyMetricsService)
3 changes: 3 additions & 0 deletions backend/src/controllers/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
from .balance import balance_page
from .utxos import utxo_page
from .transactions import transactions_page
from .fees import fees_api
from .wallet import wallet_api

from .health_check import health_check_api

from .hardware_wallets import hardware_wallet_api

from .privacy_metrics import privacy_metrics_api
84 changes: 84 additions & 0 deletions backend/src/controllers/privacy_metrics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
from flask import Blueprint
import json

from src.my_types.controller_types.privacy_metrics_dtos import (
AnalyzeTxPrivacyRequestDto,
)
from src.services import PrivacyMetricsService
from dependency_injector.wiring import inject, Provide
from src.containers.service_container import ServiceContainer
from flask import request
import structlog

from src.my_types import (
GetAllPrivacyMetricsResponseDto,
PrivacyMetricDto,
AnalyzeTxPrivacyResponseDto,
)
from src.my_types.controller_types.generic_response_types import SimpleErrorResponse

privacy_metrics_api = Blueprint(
"privacy_metrics", __name__, url_prefix="/privacy-metrics"
)

LOGGER = structlog.get_logger()


@privacy_metrics_api.route("/")
@inject
def get_privacy_metrics(
privacy_service: PrivacyMetricsService = Provide[
ServiceContainer.privacy_metrics_service
],
):
"""
Get all privacy metrics.
"""
try:
all_metrics = privacy_service.get_all_privacy_metrics()

return GetAllPrivacyMetricsResponseDto.model_validate(
dict(
metrics=[
PrivacyMetricDto(
name=privacy_metric.name,
display_name=privacy_metric.display_name,
description=privacy_metric.description,
)
for privacy_metric in all_metrics
]
)
).model_dump()

except Exception as e:
LOGGER.error("error getting privacy metrics", error=e)
return SimpleErrorResponse(message="error getting privacy metrics").model_dump()


@privacy_metrics_api.route("/", methods=["POST"])
@inject
def anaylze_tx_privacy(
privacy_service: PrivacyMetricsService = Provide[
ServiceContainer.privacy_metrics_service
],
):
"""
Analyze a selected transaction based on an array of selected privacy metrics.
"""
try:
request_data = AnalyzeTxPrivacyRequestDto.model_validate(
json.loads(request.data)
)
results = privacy_service.analyze_tx_privacy(
request_data.txid, request_data.privacy_metrics
)

return AnalyzeTxPrivacyResponseDto.model_validate(
dict(results=results)
).model_dump()

except Exception as e:
LOGGER.error("error analzying transaction privacy metrics", error=e)
return SimpleErrorResponse(
message="error analzying transaction privacy metrics"
).model_dump()
Loading

0 comments on commit 548774e

Please sign in to comment.