mirror of
https://github.com/Labelbase/Labelbase.git
synced 2026-08-13 12:33:23 +02:00
.
This commit is contained in:
parent
82394dec51
commit
f18bce10b0
5 changed files with 240 additions and 29 deletions
54
django/labelbase/migrations/0012_auto_20251122_0800.py
Normal file
54
django/labelbase/migrations/0012_auto_20251122_0800.py
Normal 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)),
|
||||
),
|
||||
]
|
||||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue