mirror of
https://github.com/Labelbase/Labelbase.git
synced 2026-08-19 13:18:23 +02:00
314 lines
13 KiB
HTML
314 lines
13 KiB
HTML
{% extends "_base.html" %}
|
|
{% load i18n %}
|
|
{% load sekizai_tags %}
|
|
{% load labelbase_tags %}
|
|
{% load backgroundtask_tags %}
|
|
{% load attachments_tags %}
|
|
|
|
{% block title %}{{ object.type }} {{ object.ref }}{% endblock %}
|
|
|
|
{% block content %}
|
|
|
|
{% get_attachments_for object.get_label_attachment as my_attachments %}
|
|
|
|
<div class="row">
|
|
<ul class="nav nav-tabs" style="padding-top: 2rem;">
|
|
<li class="nav-item">
|
|
<a class="nav-link {% if action == "update" %}active{% endif %}" aria-current="page" href="{% url 'edit_label' object.id %}">Label Detail</a>
|
|
</li>
|
|
|
|
{% if object.type == "tx" %}
|
|
<li class="nav-item">
|
|
<a class="nav-link {% if action == "labeling" %}active{% endif %}" href="{% url 'edit_label' object.id %}labeling/">Transaction Labeling</a>
|
|
</li>
|
|
{% endif %}
|
|
|
|
{% if object.type == "output" %}
|
|
<li class="nav-item">
|
|
<a class="nav-link {% if action == "output-details" %}active{% endif %}" href="{% url 'edit_label' object.id %}output-details/">Output Details</a>
|
|
</li>
|
|
{% endif %}
|
|
|
|
{% if object.type == "xpub" %}
|
|
<li class="nav-item">
|
|
{% switch object.ref|slice:":4" %}
|
|
{% case "xpub" %}
|
|
{% case "tpub" %}
|
|
<a class="nav-link {% if action == "derive-addresses" %}active{% endif %}" href="{% url 'edit_label' object.id %}derive-addresses/?derivation=m/44&address_count={{ address_count }}&offset={{ offset }}">Derive Addresses</a>
|
|
{% case "ypub" %}
|
|
{% case "upub" %}
|
|
<a class="nav-link {% if action == "derive-addresses" %}active{% endif %}" href="{% url 'edit_label' object.id %}derive-addresses/?derivation=m/49&address_count={{ address_count }}&offset={{ offset }}">Derive Addresses</a>
|
|
{% case "zpub" %}
|
|
{% case "vpub" %}
|
|
<a class="nav-link {% if action == "derive-addresses" %}active{% endif %}" href="{% url 'edit_label' object.id %}derive-addresses/?derivation=m/84&address_count={{ address_count }}&offset={{ offset }}">Derive Addresses</a>
|
|
{% else %}
|
|
<a class="nav-link {% if action == "derive-addresses" %}active{% endif %}" href="{% url 'edit_label' object.id %}derive-addresses/">Derive Addresses</a>
|
|
{% endswitch %}
|
|
</li>
|
|
{% endif %}
|
|
|
|
{% if object.labelbase.user.profile.use_attachments %}
|
|
<li class="nav-item">
|
|
{% is_self_hosted as my_labelbase_is_self_hosted %}
|
|
{% if my_labelbase_is_self_hosted %}
|
|
<a class="nav-link {% if action == "attachments" %}active{% endif %}" aria-current="page" href="{% url 'edit_label' object.id %}attachments/">
|
|
Attachments
|
|
{% if my_attachments.count %}
|
|
<span class="badge text-bg-secondary top-0 rounded-pill">{% attachments_count object.get_label_attachment %}</span>
|
|
{% endif %}
|
|
</a>
|
|
{% else %}
|
|
<a class="nav-link disabled">Attachments</a>
|
|
{% endif %}
|
|
</li>
|
|
{% endif %}
|
|
|
|
{% if object.ref == "54e48e5f5c656b26c3bca14a8c95aa583d07ebe84dde3b7dd4a78f4e4186e713" %}
|
|
<li class="nav-item">
|
|
<a class="nav-link" href="/static/bitcoin.pdf">Bitcoin White Paper</a>
|
|
</li>
|
|
{% endif %}
|
|
</ul>
|
|
</div>
|
|
|
|
<div style="padding-top: 0rem; padding-bottom: 2.1rem; ">
|
|
|
|
{% if output.last_error %}
|
|
<div class="alert bd-callout bd-callout-danger">
|
|
<div class="btn-group" role="group" style="position: absolute; top: -8px; right: -4px; padding: 1.25rem 1rem;">
|
|
<button type="button" class="btn btn-sm btn-outline-danger dropdown-toggle " data-bs-toggle="dropdown" aria-expanded="false">
|
|
Actions
|
|
</button>
|
|
<ul class="dropdown-menu">
|
|
<li><a class="dropdown-item" href="{% url 'outputstat_update_redirect' output_stats_id=output.id label_id=object.id %}?force-spent=none">Verify output status</a></li>
|
|
<li><a class="dropdown-item" href="{% url 'outputstat_update_redirect' output_stats_id=output.id label_id=object.id %}?force-spent=true">Mark output as spent</a></li>
|
|
<li><a class="dropdown-item" href="{% url 'outputstat_update_redirect' output_stats_id=output.id label_id=object.id %}?force-spent=false">Mark output as unspent</a></li>
|
|
</ul>
|
|
</div>
|
|
<strong>Ouch!</strong> Got "{{ output.last_error.message }}{% if output.last_error.code %} (code {{ output.last_error.code }}){% endif %}" from Electrum.
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% is_label_id_in_queue object.id as is_in_queue %}
|
|
{% if is_in_queue %}
|
|
<div class="alert bd-callout bd-callout-info">
|
|
<strong>Output in queue!</strong> This output is currently in the processing queue. It will be checked shortly.
|
|
</div>
|
|
{% else %}
|
|
{% if object.type == "output" %}
|
|
{% if output.get_spent_status == "spent" %}
|
|
<div class="bd-callout bd-callout-warning">
|
|
<strong>Output spent!</strong> Blockchain records indicate that this output has been spent in another transaction.
|
|
</div>
|
|
{% elif output.get_spent_status == "unspent" %}
|
|
<div class="bd-callout bd-callout-good">
|
|
<strong>Output unspent!</strong> Blockchain records indicate that this output has not been spent yet.
|
|
</div>
|
|
{% elif output.get_spent_status == "unconfirmed" %}
|
|
<div class="alert bd-callout bd-callout-info">
|
|
<div class="btn-group" role="group" style="position: absolute; top: -8px; right: -4px; padding: 1.25rem 1rem;">
|
|
<button type="button" class="btn btn-sm btn-outline-info dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
|
Actions
|
|
</button>
|
|
<ul class="dropdown-menu">
|
|
<li>
|
|
<a class="dropdown-item" href="{% url 'outputstat_update_redirect' output_stats_id=output.id label_id=object.id %}?force-spent=none">
|
|
Verify output status
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
<strong>Output unconfirmed!</strong> Blockchain records indicate that this output has not been confirmed yet.
|
|
</div>
|
|
{% else %}
|
|
<div class="bd-callout bd-callout-warning">
|
|
<strong>Unknown status:</strong> The status of this output could not be determined.
|
|
</div>
|
|
{% endif %}
|
|
{% endif %}
|
|
{% endif %}
|
|
|
|
{% block label_edit_content %}
|
|
{% endblock %}
|
|
|
|
</div>
|
|
|
|
<div class="modal" tabindex="-1" id="deleteLabelModal">
|
|
<div class="modal-dialog modal-dialog-centered">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Delete label?</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="alert alert-warning" role="alert">
|
|
<strong>Warning:</strong>
|
|
<br>
|
|
<br>
|
|
This action will permanently delete the label and cannot be undone. Are you sure you want to proceed?
|
|
</div>
|
|
</div>
|
|
<form method="post" action="{% url 'del_label' object.pk %}">{% csrf_token %}
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">CANCEL</button>
|
|
<button type="submit" class="btn btn-danger" data-bs-dismiss="modal">DELETE</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
{% addtoblock "js" %}
|
|
|
|
// BIP-329 field visibility based on type
|
|
const fieldsByType = {
|
|
'tx': ['origin', 'height', 'time', 'fee', 'value', 'rate'],
|
|
'addr': ['origin', 'keypath', 'heights'],
|
|
'pubkey': ['origin', 'keypath'],
|
|
'input': ['origin', 'keypath', 'value', 'fmv', 'height', 'time'],
|
|
'output': ['origin', 'spendable', 'keypath', 'value', 'fmv', 'height', 'time'],
|
|
'xpub': ['origin']
|
|
};
|
|
|
|
function updateFieldVisibility() {
|
|
const typeField = document.getElementById('id_type');
|
|
if (!typeField) return;
|
|
|
|
const selectedType = typeField.value;
|
|
const allowedFields = fieldsByType[selectedType] || [];
|
|
|
|
// All additional fields (excluding core fields: type, ref, label)
|
|
const allAdditionalFields = ['origin', 'spendable', 'height', 'time', 'fee', 'value', 'rate', 'keypath', 'fmv', 'heights'];
|
|
|
|
allAdditionalFields.forEach(fieldName => {
|
|
const field = document.getElementById(`id_${fieldName}`);
|
|
if (field) {
|
|
// Find the parent form-group/control-group div
|
|
const wrapper = field.closest('.form-group') || field.closest('.control-group') || field.closest('.mb-3') || field.parentElement.parentElement;
|
|
|
|
if (wrapper) {
|
|
if (allowedFields.includes(fieldName)) {
|
|
wrapper.style.display = '';
|
|
field.removeAttribute('disabled');
|
|
} else {
|
|
wrapper.style.display = 'none';
|
|
field.setAttribute('disabled', 'disabled');
|
|
field.value = ''; // Clear hidden fields
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Run on page load
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
updateFieldVisibility();
|
|
|
|
// Run when type changes
|
|
const typeField = document.getElementById('id_type');
|
|
if (typeField) {
|
|
typeField.addEventListener('change', updateFieldVisibility);
|
|
}
|
|
});
|
|
|
|
// Validate fmv field (JSON object with currency codes)
|
|
const fmvField = document.getElementById('id_fmv');
|
|
if (fmvField) {
|
|
fmvField.addEventListener('blur', function() {
|
|
try {
|
|
if (this.value && this.value.trim()) {
|
|
const parsed = JSON.parse(this.value);
|
|
if (typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
alert('FMV must be a JSON object like {"USD": 1233.45}');
|
|
return;
|
|
}
|
|
// Validate values are numeric (accept both numbers and numeric strings)
|
|
for (const [key, value] of Object.entries(parsed)) {
|
|
// Check if it's a number OR a numeric string
|
|
const isNumeric = typeof value === 'number' ||
|
|
(typeof value === 'string' && !isNaN(parseFloat(value)) && isFinite(value));
|
|
if (!isNumeric) {
|
|
alert(`FMV value for ${key} must be numeric (got: ${typeof value})`);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
} catch (e) {
|
|
alert('Invalid JSON format for fmv field');
|
|
}
|
|
});
|
|
}
|
|
|
|
// Validate rate field (JSON object with currency codes)
|
|
const rateField = document.getElementById('id_rate');
|
|
if (rateField) {
|
|
rateField.addEventListener('blur', function() {
|
|
try {
|
|
if (this.value && this.value.trim()) {
|
|
const parsed = JSON.parse(this.value);
|
|
if (typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
alert('Rate must be a JSON object like {"USD": 105620.00}');
|
|
return;
|
|
}
|
|
// Validate values are numeric (accept both numbers and numeric strings)
|
|
for (const [key, value] of Object.entries(parsed)) {
|
|
// Check if it's a number OR a numeric string
|
|
const isNumeric = typeof value === 'number' ||
|
|
(typeof value === 'string' && !isNaN(parseFloat(value)) && isFinite(value));
|
|
if (!isNumeric) {
|
|
alert(`Rate value for ${key} must be numeric (got: ${typeof value})`);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
} catch (e) {
|
|
alert('Invalid JSON format for rate field');
|
|
}
|
|
});
|
|
}
|
|
|
|
// Validate heights field (JSON array of integers)
|
|
const heightsField = document.getElementById('id_heights');
|
|
if (heightsField) {
|
|
heightsField.addEventListener('blur', function() {
|
|
try {
|
|
if (this.value && this.value.trim()) {
|
|
const parsed = JSON.parse(this.value);
|
|
if (!Array.isArray(parsed)) {
|
|
alert('Heights must be a JSON array like [123456, 789012]');
|
|
return;
|
|
}
|
|
// Validate all values are integers
|
|
for (const height of parsed) {
|
|
if (!Number.isInteger(height)) {
|
|
alert('All heights must be integers');
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
} catch (e) {
|
|
alert('Invalid JSON format for heights field');
|
|
}
|
|
});
|
|
}
|
|
|
|
// Validate integer fields (height, fee, value)
|
|
const integerFields = ['id_height', 'id_fee', 'id_value'];
|
|
integerFields.forEach(fieldId => {
|
|
const field = document.getElementById(fieldId);
|
|
if (field) {
|
|
field.addEventListener('blur', function() {
|
|
if (this.value && this.value.trim()) {
|
|
if (!/^-?\d+$/.test(this.value.trim())) {
|
|
alert(`${fieldId.replace('id_', '')} must be an integer`);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
{% endaddtoblock %}
|
|
</script>
|
|
|
|
{% endblock %}
|