Skip to content

Commit

Permalink
Merge pull request #9 from Jwyman328/release-0.7.0
Browse files Browse the repository at this point in the history
Release 0.7.0
  • Loading branch information
Jwyman328 authored Sep 8, 2024
2 parents 0671163 + bc28a0c commit eed0b83
Show file tree
Hide file tree
Showing 20 changed files with 1,755 additions and 749 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# 0.7.0
- Add consolidation mode.

# 0.6.0
- Improve fee estimate
Expand Down
2 changes: 0 additions & 2 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,6 @@


# Feature Ideas
- consolidation comparison, and create psbt at end of process.
- ability to toggle fee rate comparing the prevous utxos vs. the hypothetical new one and how they fare in different fee rates
- consolidation recommendation support.
- utxo cost breakdown.
- individual non wallet fee estimation.
Expand Down
2 changes: 1 addition & 1 deletion backend/setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

[metadata]
name = local_family_wallet
version = 0.6.0
version = 0.7.0

[options]
packages = src
Expand Down
8 changes: 8 additions & 0 deletions backend/src/controllers/utxos.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ def get_fee_for_utxo(
):
"""
Get a fee estimate for any number of utxos as input.
Optionally include a psbt in the response.
To find the utxos, we need to know the txid and vout values.
"""

Expand All @@ -37,6 +38,7 @@ def get_fee_for_utxo(
fee_rate=request.args.get("feeRate"),
transactions=json.loads(transactions_request_data),
output_count=request.args.get("outputCount"),
include_psbt=request.args.get("includePsbt", False),
)
)

Expand All @@ -54,9 +56,15 @@ def get_fee_for_utxo(
fee_estimate_response.status == "success"
and fee_estimate_response.data is not None
):
base_64_psbt = (
fee_estimate_response.psbt.serialize()
if fee_estimate_response.psbt and get_utxos_request_dto.include_psbt
else None
)
return GetUtxosResponseDto(
spendable=True,
fee=fee_estimate_response.data.fee,
psbt=base_64_psbt,
).model_dump()

if fee_estimate_response.status == "unspendable":
Expand Down
8 changes: 5 additions & 3 deletions backend/src/services/wallet/wallet.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class BuildTransactionResponseType:
class GetFeeEstimateForUtxoResponseType:
status: Literal["success", "unspendable", "error"]
data: Optional[FeeDetails]
psbt: Optional[bdk.PartiallySignedTransaction]


class WalletService:
Expand Down Expand Up @@ -353,17 +354,18 @@ def get_fee_estimate_for_utxos(
if tx_response.status == "success" and tx_response.data is not None:
built_transaction = tx_response.data
fee = built_transaction.transaction_details.fee
psbt = built_transaction.psbt

if fee is not None:
total = fee + built_transaction.transaction_details.sent
percent_fee_is_of_utxo: float = (fee / total) * 100
return GetFeeEstimateForUtxoResponseType(
"success", FeeDetails(percent_fee_is_of_utxo, fee)
"success", FeeDetails(percent_fee_is_of_utxo, fee), psbt
)
else:
return GetFeeEstimateForUtxoResponseType("error", None)
return GetFeeEstimateForUtxoResponseType("error", None, None)
else:
return GetFeeEstimateForUtxoResponseType(tx_response.status, None)
return GetFeeEstimateForUtxoResponseType(tx_response.status, None, None)

def get_fee_estimate_for_utxos_from_request(
self, get_utxos_request_dto: GetUtxosRequestDto
Expand Down
117 changes: 113 additions & 4 deletions backend/src/tests/controller_tests/test_utxos_controller.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
from unittest import TestCase
from unittest.mock import MagicMock

from unittest.mock import MagicMock, Mock
from src.app import AppCreator
from src.services.wallet.wallet import GetFeeEstimateForUtxoResponseType, WalletService
from src.types import FeeDetails
from src.types import GetUtxosRequestDto
from src.tests.mocks import local_utxo_mock
from bdkpython import bdk
import json


Expand Down Expand Up @@ -41,7 +43,7 @@ def test_get_fee_for_utxo_success(self):
self.mock_fee_details = FeeDetails(0.1, 100)
mock_get_fee_estimate_for_utxos_from_request = MagicMock(
return_value=GetFeeEstimateForUtxoResponseType(
"success", self.mock_fee_details
"success", self.mock_fee_details, psbt=None
)
)
with self.app.container.wallet_service.override(self.mock_wallet_service):
Expand Down Expand Up @@ -78,11 +80,118 @@ def test_get_fee_for_utxo_success(self):
assert json.loads(response.data) == {
"spendable": True,
"fee": self.mock_fee_details.fee,
"psbt": None,
}

def test_get_fee_for_utxo_success_with_psbt(self):
self.mock_fee_details = FeeDetails(0.1, 100)

mock_psbt = Mock(bdk.PartiallySignedTransaction)
mock_psbt_base64 = "mock_psbt_base64"
mock_psbt.serialize.return_value = mock_psbt_base64
mock_get_fee_estimate_for_utxos_from_request = MagicMock(
return_value=GetFeeEstimateForUtxoResponseType(
"success", self.mock_fee_details, psbt=mock_psbt
)
)
with self.app.container.wallet_service.override(self.mock_wallet_service):
self.mock_wallet_service.get_fee_estimate_for_utxos_from_request = (
mock_get_fee_estimate_for_utxos_from_request
)

fee_rate = "5"
outputCount = "2"

# /{transaction_id}/{vout} put this in the request args
transactions = [
{
"id": f"{local_utxo_mock.outpoint.txid}",
"vout": f"{local_utxo_mock.outpoint.vout}",
},
]
response = self.test_client.post(
"/utxos/fees",
query_string={
"feeRate": fee_rate,
"outputCount": outputCount,
"includePsbt": True,
},
json=transactions,
)

mock_get_fee_estimate_for_utxos_from_request.assert_called_with(
GetUtxosRequestDto.model_validate(
dict(
transactions=transactions,
fee_rate=fee_rate,
output_count=outputCount,
include_psbt=True,
)
)
)

assert json.loads(response.data) == {
"spendable": True,
"fee": self.mock_fee_details.fee,
"psbt": mock_psbt_base64,
}

def test_get_fee_for_utxo_success_with_includePsbt_false(self):
self.mock_fee_details = FeeDetails(0.1, 100)

mock_psbt = Mock(bdk.PartiallySignedTransaction)
mock_psbt_base64 = "mock_psbt_base64"
mock_psbt.serialize.return_value = mock_psbt_base64
mock_get_fee_estimate_for_utxos_from_request = MagicMock(
return_value=GetFeeEstimateForUtxoResponseType(
"success", self.mock_fee_details, psbt=mock_psbt
)
)
with self.app.container.wallet_service.override(self.mock_wallet_service):
self.mock_wallet_service.get_fee_estimate_for_utxos_from_request = (
mock_get_fee_estimate_for_utxos_from_request
)

fee_rate = "5"
outputCount = "2"

# /{transaction_id}/{vout} put this in the request args
transactions = [
{
"id": f"{local_utxo_mock.outpoint.txid}",
"vout": f"{local_utxo_mock.outpoint.vout}",
},
]
response = self.test_client.post(
"/utxos/fees",
query_string={
"feeRate": fee_rate,
"outputCount": outputCount,
"includePsbt": False,
},
json=transactions,
)

mock_get_fee_estimate_for_utxos_from_request.assert_called_with(
GetUtxosRequestDto.model_validate(
dict(
transactions=transactions,
fee_rate=fee_rate,
output_count=outputCount,
include_psbt=False,
)
)
)

assert json.loads(response.data) == {
"spendable": True,
"fee": self.mock_fee_details.fee,
"psbt": None,
}

def test_get_fee_for_utxo_unspendable_error(self):
mock_get_fee_estimate_for_utxos_from_request = MagicMock(
return_value=GetFeeEstimateForUtxoResponseType("unspendable", None)
return_value=GetFeeEstimateForUtxoResponseType("unspendable", None, None)
)
with self.app.container.wallet_service.override(self.mock_wallet_service):
self.mock_wallet_service.get_fee_estimate_for_utxos_from_request = (
Expand Down Expand Up @@ -121,7 +230,7 @@ def test_get_fee_for_utxo_unspendable_error(self):

def test_get_fee_for_utxo_error(self):
mock_get_fee_estimate_for_utxos_from_request = MagicMock(
return_value=GetFeeEstimateForUtxoResponseType("error", None)
return_value=GetFeeEstimateForUtxoResponseType("error", None, None)
)
with self.app.container.wallet_service.override(self.mock_wallet_service):
self.mock_wallet_service.get_fee_estimate_for_utxos_from_request = (
Expand Down
4 changes: 3 additions & 1 deletion backend/src/tests/service_tests/test_wallet_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -680,6 +680,7 @@ def test_get_fee_estimate_for_utxos(self):
fee: int = cast(int, transaction_details_mock.fee)
expected_fee_percent = (fee / (transaction_details_mock.sent + fee)) * 100
assert fee_estimate_response.data == FeeDetails(expected_fee_percent, fee)
assert fee_estimate_response.psbt == "mock_psbt"

def test_get_fee_estimate_for_utxo_with_build_tx_unspendable(self):
build_transaction_error_response = BuildTransactionResponseType(
Expand Down Expand Up @@ -725,8 +726,9 @@ def test_get_fee_estimate_for_utxos_from_request(self):
dict(transactions=transactions, fee_rate=fee_rate, output_count="2")
)

mock_psbt = Mock()
mock_fee_estimates_response = GetFeeEstimateForUtxoResponseType(
status="success", data=FeeDetails(0.1, 100)
status="success", data=FeeDetails(0.1, 100), psbt=mock_psbt
)

with (
Expand Down
3 changes: 3 additions & 0 deletions backend/src/types/controller_types/utxos_dtos.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from typing import Optional
from pydantic import BaseModel, field_validator, Field
import structlog

Expand All @@ -14,6 +15,7 @@ class GetUtxosRequestDto(BaseModel):
transactions: list[TransactionDto]
# default to two outputs, one for the recipient and one for the change
output_count: str = Field(default="2")
include_psbt: Optional[bool] = Field(default=False)

@field_validator("transactions")
def check_empty_transactions_list(cls, v):
Expand All @@ -26,6 +28,7 @@ def check_empty_transactions_list(cls, v):
class GetUtxosResponseDto(BaseModel):
spendable: bool
fee: int
psbt: Optional[str]


class GetUtxosErrorResponseDto(BaseModel):
Expand Down
4 changes: 2 additions & 2 deletions release/app/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion release/app/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "LiveWallet",
"version": "0.6.0",
"version": "0.7.0",
"description": "An application to understand the health of your Bitcoin wallet.",
"license": "MIT",
"author": {
Expand Down
3 changes: 2 additions & 1 deletion src/app/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,10 @@ export class ApiClient {
utxos: UtxoRequestParam[],
feeRate: number = 1,
outputCount: number = 1,
includePsbtInResponse: boolean = false,
) {
const response = await fetchHandler(
`${configs.backendServerBaseUrl}/utxos/fees?feeRate=${feeRate}&outputCount=${outputCount}`,
`${configs.backendServerBaseUrl}/utxos/fees?feeRate=${feeRate}&outputCount=${outputCount}&includePsbt=${includePsbtInResponse}`,
'POST',
utxos,
);
Expand Down
1 change: 1 addition & 0 deletions src/app/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export type CurrentFeesResponseType = {
export type CreateTxFeeEstimationResponseType = {
spendable: boolean;
fee: number;
psbt?: string; // base64 of psbt
};

export type InitiateWalletResponseType = {
Expand Down
Loading

0 comments on commit eed0b83

Please sign in to comment.