diff --git a/django/importer/__init__.py b/django/importer/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/django/importer/apps.py b/django/importer/apps.py new file mode 100644 index 0000000..ff9040f --- /dev/null +++ b/django/importer/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class ImporterConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "importer" diff --git a/django/importer/forms.py b/django/importer/forms.py new file mode 100644 index 0000000..24911fa --- /dev/null +++ b/django/importer/forms.py @@ -0,0 +1,17 @@ +from django import forms + +IMPORTER_CHOICES = ( + ("BIP-0329", "BIP-329 .jsonl"), + #("BIP-0329-7z-enc" , "BIP-329 (encrypted) .7z"), + ("csv-bluewallet", "BlueWallet .csv"), + ("csv-bitbox", "BitBox .csv"), + ("pocket-accointing", "Pocket Accointing .csv") +) + + +class UploadFileForm(forms.Form): + labelbase_id = forms.IntegerField(widget=forms.HiddenInput()) + import_type = forms.ChoiceField( + choices=IMPORTER_CHOICES #, widget=forms.HiddenInput() + ) + file = forms.FileField() diff --git a/django/importer/pocket.py b/django/importer/pocket.py new file mode 100644 index 0000000..bbc88ca --- /dev/null +++ b/django/importer/pocket.py @@ -0,0 +1,49 @@ +import csv +import json +from labelbase.models import Label +from labelbase.models import Labelbase + + +def validate_csv_format(csv_file_path): + expected_sequence = ['withdraw', 'order', 'deposit'] + with open(csv_file_path, newline='') as csvfile: + csv_reader = csv.reader(csvfile) + next(csv_reader) # Skip the header + for row in csv_reader: + if len(row) != 11 or row[0] not in expected_sequence: + return False + expected_index = expected_sequence.index(row[0]) + if expected_index == 0 and expected_index != 0: + return False + expected_sequence.append(expected_sequence.pop(0)) + return True + + +def parse_csv_to_json(csv_file_path): + json_list = [] + with open(csv_file_path, newline='') as csvfile: + csv_reader = csv.reader(csvfile) + next(csv_reader) # Skip the header + group = [] + for row in csv_reader: + if row[0] == 'withdraw': + if group: + json_list.append(group) + group = [] + data = { + 'transactionType': row[0], + 'date': row[1], + 'inBuyAmount': row[2], + 'inBuyAsset': row[3], + 'outSellAmount': row[4], + 'outSellAsset': row[5], + 'feeAmount': row[6] if row[6] else "", + 'feeAsset': row[7] if row[7] else "", + 'classification': row[8] if row[8] else "", + 'operationId': row[9] if row[9] else "", + 'comments': row[10] if row[10] else "" + } + group.append(data) + if group: + json_list.append(group) + return json_list diff --git a/django/importer/templatetags/__init__.py b/django/importer/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/django/importer/templatetags/importer_tags.py b/django/importer/templatetags/importer_tags.py new file mode 100644 index 0000000..abc4fbf --- /dev/null +++ b/django/importer/templatetags/importer_tags.py @@ -0,0 +1,34 @@ +from django import template +from importer.forms import UploadFileForm + +register = template.Library() + +@register.simple_tag +def genericlabeluploadform(labelbase_id): + form = UploadFileForm() + form.fields["labelbase_id"].initial = labelbase_id + form.fields["import_type"].initial = "BIP-0329" + return form + +@register.simple_tag +def bip0329labeluploadform(labelbase_id): + form = UploadFileForm() + form.fields["labelbase_id"].initial = labelbase_id + form.fields["import_type"].initial = "BIP-0329" + return form + + +@register.simple_tag +def csvBlueWalletlabeluploadform(labelbase_id): + form = UploadFileForm() + form.fields["labelbase_id"].initial = labelbase_id + form.fields["import_type"].initial = "csv-bluewallet" + return form + + +@register.simple_tag +def csvBitBoxLabeluploadform(labelbase_id): + form = UploadFileForm() + form.fields["labelbase_id"].initial = labelbase_id + form.fields["import_type"].initial = "csv-bitbox" + return form diff --git a/django/importer/views.py b/django/importer/views.py new file mode 100644 index 0000000..c9a5acc --- /dev/null +++ b/django/importer/views.py @@ -0,0 +1,184 @@ +from django.http import HttpResponseRedirect +from django.shortcuts import render +from django.contrib.auth.decorators import login_required +import os +import json +from labelbase.models import Labelbase +from labelbase.serializers import LabelSerializer +from django.shortcuts import get_object_or_404 +from django.contrib import messages +import decimal + + +from .forms import UploadFileForm +from tempfile import NamedTemporaryFile + +EOLSTOP = [b"", "", None, "\n"] + + +def handle_uploaded_file(f): + fp = NamedTemporaryFile(delete=False) + for chunk in f.chunks(): + fp.write(chunk) + return fp + + +@login_required +def upload_labels(request): + """ + Used to import labels manually using files. + """ + + if request.method == "POST": + form = UploadFileForm(request.POST, request.FILES) + if form.is_valid(): + imported_lables = 0 + labelbase = get_object_or_404( + Labelbase, + id=form.cleaned_data.get("labelbase_id", ""), + user_id=request.user.id, + ) + fp = handle_uploaded_file(request.FILES["file"]) + fp.seek(0) + # BIP-0329 + if form.cleaned_data.get("import_type", "") == "BIP-0329": + while True: + buf = fp.readline() + if buf in EOLSTOP: + break + data = json.loads(buf) + data["labelbase"] = labelbase.id + serializer = LabelSerializer(data=data) + if serializer.is_valid(): + serializer.save() + imported_lables += 1 + elif form.cleaned_data.get("import_type", "") == "BIP-0329-7z-enc": + # TODO: Implement + pass + # Bitbox App + elif form.cleaned_data.get("import_type", "") == "csv-bitbox": + while True: + buf = fp.readline() + if buf in EOLSTOP: + break + try: + buf = str(buf.decode("utf-8")) + sbuf = buf.split(",") + # Time,Type,Amount,Unit,Fee,Address,Transaction ID,Note + for elem in [("tx", 6), ("addr", 5)]: + data = { + "type": elem[0], + "ref": sbuf[elem[1]], + "label": " ".join(sbuf[7:]), + } + data["labelbase"] = labelbase.id + serializer = LabelSerializer(data=data) + if serializer.is_valid(): + serializer.save() + imported_lables += 1 + else: + messages.add_message( + request, + messages.ERROR, + 'Could not process line "{}".'.format(buf), + ) + except Exception as ex: + messages.add_message( + request, + messages.ERROR, + 'Could not process line "{}", {}.'.format(buf, ex), + ) + # Pocket Accointing + elif form.cleaned_data.get("import_type", "") == "pocket-accointing": + fp.close() + csv_file_path = fp.name + mempool_api = labelbase.get_mempool_api() + from .pocket import validate_csv_format, parse_csv_to_json + if validate_csv_format(csv_file_path): + for item in parse_csv_to_json(csv_file_path): + #label = "Got {} {} for {} {} in tx=\"{}\" ref=\"{} #Pocket\" ".format(item[0].get('outSellAmount'), item[1].get('inBuyAsset') , item[2].get('inBuyAmount'), item[2].get('inBuyAsset') , item[0].get('operationId'), item[1].get('operationId')) + + label = "Got {} {} for {:.2f} {} with reference: {} #Pocket".format(item[0].get('outSellAmount'), item[1].get('inBuyAsset'), decimal.Decimal(item[2].get('inBuyAmount')), item[2].get('inBuyAsset'), item[1].get('operationId')) + txid = item[0].get('operationId') + tx = mempool_api.get_transaction(txid) + potential_utxos = [] + vouts = tx.get("vout", []) + for i in range(len(vouts)): + if vouts[i].get('value', 0) == decimal.Decimal(item[0].get('outSellAmount'))*100000000: + potential_utxos.append("{}:{}".format(txid, i )) + data = {} + if len(potential_utxos) == 1: + # label UTXO/output of tx + data = { + "type": "output", + "ref": potential_utxos[0], + "label": label, + } + if len(potential_utxos) > 1: + # mark tx, add warning tag + data = { + "type": "tx", + "ref": txid, + "label": "{} {}".format(label, "#W001_UTXO_NOT_FOUND"), + } + if data: + data["labelbase"] = labelbase.id + serializer = LabelSerializer(data=data) + if serializer.is_valid(): + serializer.save() + imported_lables += 1 + else: + messages.add_message( + request, + messages.ERROR, + 'Could not process record "{}".'.format(item), + ) + else: + print("ERROR") # TODO + # BlueWallet + elif form.cleaned_data.get("import_type", "") == "csv-bluewallet": + while True: + buf = fp.readline() + if buf in EOLSTOP: + break + try: + buf = str(buf.decode("utf-8")) + sbuf = buf.split(",") + data = { + "type": "tx", + "ref": sbuf[1], + "label": " ".join(sbuf[3:]), + } + data["labelbase"] = labelbase.id + serializer = LabelSerializer(data=data) + if serializer.is_valid(): + serializer.save() + imported_lables += 1 + else: + messages.add_message( + request, + messages.ERROR, + 'Could not process line "{}".'.format(buf), + ) + except Exception as ex: + messages.add_message( + request, + messages.ERROR, + 'Could not process line "{}", {}.'.format(buf, ex), + ) + else: + fp.close() + os.unlink(fp.name) + return HttpResponseRedirect("/failed/url/") + fp.close() + os.unlink(fp.name) + if imported_lables: + messages.add_message( + request, + messages.INFO, + "Processed and imported {} labels.".format(imported_lables), + ) + return HttpResponseRedirect(labelbase.get_absolute_url()) + else: + form = UploadFileForm() + return render(request, "upload.html", {"form": form}) diff --git a/django/templates/_base.html b/django/templates/_base.html index d831357..acfd652 100644 --- a/django/templates/_base.html +++ b/django/templates/_base.html @@ -386,7 +386,6 @@ $(document).ready(function () { - // Disable the Origin field by default if not Tx {% if label.type == "tx" %} {% else %} diff --git a/django/templates/_modal_exportLabelbaseModal.html b/django/templates/_modal_exportLabelbaseModal.html index 92b31c9..45629e3 100644 --- a/django/templates/_modal_exportLabelbaseModal.html +++ b/django/templates/_modal_exportLabelbaseModal.html @@ -17,17 +17,15 @@
We utilize Electrum servers to automatically update your spendable outputs. Labelbase keeps the state of your spendable outputs up to date, helping you keep track of your labels and funds.
+Labelbase converts the value of your spendable outputs to fiat currency, assisting you in keeping track of your portfolio value measured in your chosen currency.
+.. and much more.
+
Ready to optimize your Bitcoin labeling for optimal efficiency?
-
-Discover the power of streamlined label management with Labelbase.
-
-Join Labelbase now and unlock the full potential of your Bitcoin experience!
+
+ Discover the power of streamlined label management with Labelbase.
+
+ Join Labelbase now and unlock the full potential of your Bitcoin experience!
+ The privacy policy applies to labelbase.space hosted by Labelbase. For self-hosted versions, variations or customizations may exist.
+
+ Users are responsible for reviewing any specific privacy considerations or customizations made to their instance.
+
+ Last updated: December 19th 2023.
- "LABELBASE" (labelbase.space) is provided without any guarantees of anything, not + "LABELBASE" (labelbase.space, but also the self-hosted versions) is provided without any guarantees of anything, not even that it will do its job properly. If you have any doubts, please use an alternate service.
@@ -107,5 +107,5 @@ FIRE, FLOODS, ACCIDENTS, SERVICE OUTAGES RESULTING FROM EQUIPMENT AND/OR SOFTWAR