This commit is contained in:
Xavier Fiechter 2025-12-17 23:42:52 +01:00
parent 82394dec51
commit f18bce10b0
5 changed files with 240 additions and 29 deletions

View file

@ -0,0 +1,54 @@
# Generated by Django 3.2.25 on 2025-11-22 08:00
from django.db import migrations, models
import django_cryptography.fields
class Migration(migrations.Migration):
dependencies = [
('labelbase', '0011_alter_labelbase_operation_mode'),
]
operations = [
migrations.AddField(
model_name='label',
name='fee',
field=django_cryptography.fields.encrypt(models.CharField(blank=True, help_text='Transaction fee in satoshis (stored as string)', max_length=32, null=True)),
),
migrations.AddField(
model_name='label',
name='fmv',
field=django_cryptography.fields.encrypt(models.TextField(blank=True, help_text='Fair market value (JSON string)', null=True)),
),
migrations.AddField(
model_name='label',
name='height',
field=django_cryptography.fields.encrypt(models.CharField(blank=True, help_text='Block height where transaction was confirmed', max_length=16, null=True)),
),
migrations.AddField(
model_name='label',
name='heights',
field=django_cryptography.fields.encrypt(models.TextField(blank=True, help_text='Block heights for address activity (JSON array as string)', null=True)),
),
migrations.AddField(
model_name='label',
name='keypath',
field=django_cryptography.fields.encrypt(models.CharField(blank=True, help_text='Key derivation path (e.g., /1/123)', max_length=256, null=True)),
),
migrations.AddField(
model_name='label',
name='rate',
field=django_cryptography.fields.encrypt(models.TextField(blank=True, help_text='Exchange rate at transaction time (JSON string)', null=True)),
),
migrations.AddField(
model_name='label',
name='time',
field=django_cryptography.fields.encrypt(models.CharField(blank=True, help_text='ISO-8601 timestamp of the block', max_length=64, null=True)),
),
migrations.AddField(
model_name='label',
name='value',
field=django_cryptography.fields.encrypt(models.CharField(blank=True, help_text='Transaction value in satoshis, signed (stored as string)', max_length=32, null=True)),
),
]

View file

@ -52,7 +52,8 @@ from .views import (
CurrencySyncView,
CurrencySyncActionView,
FillMissingDataView,
FillMissingDataActionView
FillMissingDataActionView,
FillOutputFieldsActionView
)
@ -178,6 +179,9 @@ urlpatterns = [
FillMissingDataView.as_view(),
name='fill_missing_data'
),
path('label/<int:label_id>/fill-output-fields/',
FillOutputFieldsActionView.as_view(),
name='fill_output_fields_action'),
path(
'labelbase/<int:labelbase_id>/fill-missing-data/action/',
FillMissingDataActionView.as_view(),

View file

@ -1098,6 +1098,7 @@ class LabelUpdateView(UpdateView):
context["active_labelbase_id"] = self.object.labelbase.id
context["labelbase"] = self.object.labelbase
context["action"] = self.kwargs.get('action', 'update')
if context["action"] in ["labeling", "derive-addresses"]:
context["labelform"] = LabelForm(
request=self.request, labelbase_id=self.object.labelbase.id
@ -1107,15 +1108,32 @@ class LabelUpdateView(UpdateView):
mempool_api = self.object.labelbase.get_mempool_api()
context["res_tx"] = mempool_api.get_transaction(self.object.ref)
if self.object.type == "xpub":
context["address_count"] = self.request.GET.get("address_count", DEFAULT_DERIVE_ADDRESS_COUNT)
context["offset"] = int(self.request.GET.get("offset", 0))
if self.object.type == "output":
context["output"] = OutputStat.objects.filter(
user=self.object.labelbase.user,
type_ref_hash=self.object.type_ref_hash).last()
output_stat = OutputStat.objects.filter(
user=self.object.labelbase.user,
type_ref_hash=self.object.type_ref_hash
).last()
context["output"] = output_stat
# Convert Unix timestamp to human-readable UTC formats
if output_stat and output_stat.confirmed_at_block_time:
dt = datetime.utcfromtimestamp(output_stat.confirmed_at_block_time)
context["output_block_time_utc"] = dt.strftime('%Y-%m-%d %H:%M:%S')
context["output_block_time_iso"] = dt.strftime('%Y-%m-%dT%H:%M:%SZ')
# Check for missing BIP-329 fields
if context["action"] == "output-details":
missing_fields = []
applicable_fields = ['height', 'time', 'value']
for field in applicable_fields:
value = getattr(self.object, field, None)
if not value or (isinstance(value, str) and not value.strip()):
missing_fields.append(field)
context["missing_fields"] = missing_fields
return context
@ -1348,3 +1366,56 @@ class FillMissingDataActionView(View):
label.save()
return updated
class FillOutputFieldsActionView(View):
"""Single output field fill action from output-details page"""
def post(self, request, *args, **kwargs):
label_id = self.kwargs["label_id"]
label = get_object_or_404(
Label,
id=label_id,
labelbase__user_id=request.user.id,
type='output'
)
# Reuse the existing fill logic
if self._fill_label_from_outputstat(label):
messages.success(request, "Successfully filled BIP-329 fields from OutputStat data.")
else:
messages.error(request, "Could not fill fields. OutputStat data may not be available.")
return HttpResponseRedirect(
reverse('edit_label', kwargs={'pk': label_id}) + '?action=output-details'
)
def _fill_label_from_outputstat(self, label):
"""Fill a single label from OutputStat data (reused from FillMissingDataActionView)"""
output_stat = OutputStat.objects.filter(
user=label.labelbase.user,
type_ref_hash=label.type_ref_hash,
network=label.labelbase.network
).first()
if not output_stat:
return False
updated = False
if not label.height and output_stat.confirmed_at_block_height:
label.height = str(output_stat.confirmed_at_block_height)
updated = True
if not label.time and output_stat.confirmed_at_block_time:
dt = datetime.utcfromtimestamp(output_stat.confirmed_at_block_time)
label.time = dt.strftime('%Y-%m-%dT%H:%M:%SZ')
updated = True
if not label.value and output_stat.value:
label.value = str(output_stat.value)
updated = True
if updated:
label.save()
return updated

View file

@ -320,18 +320,27 @@
<div class="col d-flex align-items-start">
<div>
<h3 class="fw-bold mb-0 fs-4 text-body-emphasis">Auto-Fill Missing Data</h3>
<p>Automatically populate transaction metadata from OutputStat records. Fill block heights, timestamps, and values for your outputs with one click, saving time and ensuring accuracy.</p>
<h3 class="fw-bold mb-0 fs-4 text-body-emphasis">Bulk Auto-Fill Missing Data</h3>
<p>Automatically bulk populate transaction metadata from OutputStat records. Fill block heights, timestamps, and values for your outputs with one click, saving time and ensuring accuracy.</p>
</div>
</div>
<div class="col d-flex align-items-start">
<div>
<h3 class="fw-bold mb-0 fs-4 text-body-emphasis">One-Click BIP-329 Field Population</h3>
<p>Automatically populate transaction metadata like block height, timestamp, and value directly from OutputStat records. View missing fields at a glance and fill them with a single click from the output details page.</p>
</div>
</div>
<div class="col d-flex align-items-start">
<div>
<h3 class="fw-bold mb-0 fs-4 text-body-emphasis">Fee Health Monitoring</h3>
<p>Instantly visualize the cost-effectiveness of spending your UTXOs. Labelbase calculates and displays fee health status for each spendable output, color-coded to show if transaction fees would consume a healthy percentage of the output value.</p>
</div>
</div>
<div class="col d-flex align-items-start">
<div>
<h3 class="fw-bold mb-0 fs-4 text-body-emphasis"><br> <br> </h3>

View file

@ -1,35 +1,108 @@
{% extends "label_edit.html" %}
{% load bootstrap %}
{% load i18n %}
{% block label_edit_content %}
<!--
{{  res_tx|safe }}
-->
{% if action == "output-details" %}
{% if form.instance.type == "output" %}
<br>
<!-- ID: {{ output.id }} -->
<pre>
Output, ref: {{ label.ref }}
Output value, in sats: {{ output.value }}
Confirmed at block height: {{ output.confirmed_at_block_height }}
Confirmed at block time: {{ output.confirmed_at_block_time }}
Network: {{ output.get_network_display }}
Spent: {{ output.get_spent_status }}
{% if output.next_input_attributes %}
Fee estimation will be made based on:
<!-- BIP-329 Fields Status Alert (right after spent status in parent template) -->
{% if missing_fields %}
<div class="bd-callout bd-callout-info">
<strong>Missing BIP-329 fields:</strong>
{% for field in missing_fields %}
<span class="badge bg-warning text-dark">{{ field }}</span>
{% endfor %}
<br><small>Click the button below to populate from OutputStat data.</small>
</div>
{% else %}
<div class="bd-callout bd-callout-good">
<strong>✓ All fields populated!</strong> This label has all applicable BIP-329 fields filled.
</div>
{% endif %}
{{ output.next_input_attributes }}
<!-- Consolidated Output Details Card -->
<div class="card mb-3">
<div class="card-header">
<h5>Output Details & BIP-329 Fields</h5>
</div>
<div class="card-body">
<table class="table table-borderless">
<tr>
<td style="width: 30%;"><strong>Output ref:</strong></td>
<td><tt>{{ label.ref }}</tt></td>
</tr>
<tr>
<td><strong>Network:</strong></td>
<td>{{ output.get_network_display }}</td>
</tr>
<tr>
<td><strong>Spent status:</strong></td>
<td>{{ output.get_spent_status }}</td>
</tr>
<tr class="table-light">
<td colspan="2"><strong>BIP-329 Fields:</strong></td>
</tr>
<tr>
<td><strong>value:</strong></td>
<td>
{% if label.value %}
<tt>{{ label.value }}</tt> sats
{% else %}
<span class="text-muted">{{ output.value }} sats (available from OutputStat)</span>
{% endif %}
</td>
</tr>
<tr>
<td><strong>height:</strong></td>
<td>
{% if label.height %}
<tt>{{ label.height }}</tt>
{% else %}
<span class="text-muted">{{ output.confirmed_at_block_height }} (available from OutputStat)</span>
{% endif %}
</td>
</tr>
<tr>
<td><strong>time:</strong></td>
<td>
{% if label.time %}
<tt>{{ label.time }}</tt>
{% else %}
<span class="text-muted">{{ output_block_time_utc }} UTC (available from OutputStat)</span>
{% endif %}
</td>
</tr>
{% if output.next_input_attributes %}
<tr class="table-light">
<td colspan="2"><strong>Fee Estimation:</strong></td>
</tr>
<tr>
<td colspan="2">
{{ output.next_input_attributes }}
{% if output.next_input_attributes.input_n %}
<br><small>Assuming {{ output.next_input_attributes.input_m }}-of-{{ output.next_input_attributes.input_n }} multisig</small>
{% endif %}
</td>
</tr>
{% endif %}
</table>
{% if output.next_input_attributes.input_n %}
* Assuming {{ output.next_input_attributes.input_m }}-of-{{ output.next_input_attributes.input_n }} multisig
{% else %}
<!-- Fill Button (only show if fields are missing) -->
{% if missing_fields %}
<form method="post" action="{% url 'fill_output_fields_action' label_id=label.id %}">
{% csrf_token %}
<button type="submit" class="btn btn-primary">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-down-square" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M15 2a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V2zM0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2zm8.5 2.5a.5.5 0 0 0-1 0v5.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V4.5z"/>
</svg>
Fill Missing BIP-329 Fields from OutputStat
</button>
</form>
{% endif %}
{% endif %}
</div>
</div>
</pre>
{% endif %}
{% endif %}
{% endblock %}