From f18bce10b0381b2c35c9bdf1085109af328b6867 Mon Sep 17 00:00:00 2001 From: Xavier Fiechter Date: Wed, 17 Dec 2025 23:42:52 +0100 Subject: [PATCH] . --- .../migrations/0012_auto_20251122_0800.py | 54 ++++++++ django/labellabor/urls.py | 6 +- django/labellabor/views.py | 79 +++++++++++- django/templates/home.html | 15 ++- .../templates/label_edit_output_details.html | 115 ++++++++++++++---- 5 files changed, 240 insertions(+), 29 deletions(-) create mode 100644 django/labelbase/migrations/0012_auto_20251122_0800.py diff --git a/django/labelbase/migrations/0012_auto_20251122_0800.py b/django/labelbase/migrations/0012_auto_20251122_0800.py new file mode 100644 index 0000000..c84a26b --- /dev/null +++ b/django/labelbase/migrations/0012_auto_20251122_0800.py @@ -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)), + ), + ] diff --git a/django/labellabor/urls.py b/django/labellabor/urls.py index 0bc5db6..d6c341f 100644 --- a/django/labellabor/urls.py +++ b/django/labellabor/urls.py @@ -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//fill-output-fields/', + FillOutputFieldsActionView.as_view(), + name='fill_output_fields_action'), path( 'labelbase//fill-missing-data/action/', FillMissingDataActionView.as_view(), diff --git a/django/labellabor/views.py b/django/labellabor/views.py index deab468..f2ba030 100644 --- a/django/labellabor/views.py +++ b/django/labellabor/views.py @@ -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 diff --git a/django/templates/home.html b/django/templates/home.html index 178e505..8ee6db2 100644 --- a/django/templates/home.html +++ b/django/templates/home.html @@ -320,18 +320,27 @@
-

Auto-Fill Missing Data

-

Automatically populate transaction metadata from OutputStat records. Fill block heights, timestamps, and values for your outputs with one click, saving time and ensuring accuracy.

+

Bulk Auto-Fill Missing Data

+

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.

+
+
+

One-Click BIP-329 Field Population

+

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.

+
+
+ +

Fee Health Monitoring

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.

- + +



diff --git a/django/templates/label_edit_output_details.html b/django/templates/label_edit_output_details.html index 0025a8e..b471888 100644 --- a/django/templates/label_edit_output_details.html +++ b/django/templates/label_edit_output_details.html @@ -1,35 +1,108 @@ {% extends "label_edit.html" %} {% load bootstrap %} {% load i18n %} - {% block label_edit_content %} - + {% if action == "output-details" %} {% if form.instance.type == "output" %} -
- -
 
-    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:
+  
+  {% if missing_fields %}
+  
+ Missing BIP-329 fields: + {% for field in missing_fields %} + {{ field }} + {% endfor %} +
Click the button below to populate from OutputStat data. +
+ {% else %} +
+ ✓ All fields populated! This label has all applicable BIP-329 fields filled. +
+ {% endif %} - {{ output.next_input_attributes }} + +
+
+
Output Details & BIP-329 Fields
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + {% if output.next_input_attributes %} + + + + + + + {% endif %} +
Output ref:{{ label.ref }}
Network:{{ output.get_network_display }}
Spent status:{{ output.get_spent_status }}
BIP-329 Fields:
value: + {% if label.value %} + {{ label.value }} sats + {% else %} + {{ output.value }} sats (available from OutputStat) + {% endif %} +
height: + {% if label.height %} + {{ label.height }} + {% else %} + {{ output.confirmed_at_block_height }} (available from OutputStat) + {% endif %} +
time: + {% if label.time %} + {{ label.time }} + {% else %} + {{ output_block_time_utc }} UTC (available from OutputStat) + {% endif %} +
Fee Estimation:
+ {{ output.next_input_attributes }} + {% if output.next_input_attributes.input_n %} +
Assuming {{ output.next_input_attributes.input_m }}-of-{{ output.next_input_attributes.input_n }} multisig + {% endif %} +
- {% if output.next_input_attributes.input_n %} - * Assuming {{ output.next_input_attributes.input_m }}-of-{{ output.next_input_attributes.input_n }} multisig - {% else %} + + {% if missing_fields %} +
+ {% csrf_token %} + +
{% endif %} - {% endif %} +
+
-
{% endif %} {% endif %} {% endblock %}