diff --git a/cypress/integration/spec_wallet_utxo.js b/cypress/integration/spec_wallet_utxo.js
index 939debfe4..52f00e11c 100644
--- a/cypress/integration/spec_wallet_utxo.js
+++ b/cypress/integration/spec_wallet_utxo.js
@@ -54,12 +54,8 @@ describe('Send transactions from wallets', () => {
cy.get('tx-table').shadow().find('tx-row').eq(3).shadow().find('.select-tx-img').click()
cy.get('tx-table').shadow().find('.compose-tx-btn').click()
- // cy.get('#coin_selection_table').find('tr').eq(0).find('.coin_select_checkbox').should('be.checked')
- cy.get('.coin_select_checkbox[checked]').should('have.length', 2);
-
- cy.get('.coin_select_checkbox').eq(0).should('be.checked')
- cy.get('.coin_select_checkbox').eq(1).should('not.be.checked')
- cy.get('.coin_select_checkbox').eq(2).should('be.checked')
+ // If you select a coin from the utxo-set and cklick on "create transaction", the coins need to be preselected
+ cy.get('.coinselect-hidden').should('have.length', 3);
// Unfreeze the UTXO
cy.get('#btn_transactions').click()
@@ -80,11 +76,7 @@ describe('Send transactions from wallets', () => {
cy.get('tx-table').shadow().find('tx-row').eq(3).shadow().find('.select-tx-img').click()
cy.get('tx-table').shadow().find('.compose-tx-btn').click()
- cy.get('.coin_select_checkbox[checked]').should('have.length', 3);
+ cy.get('.coinselect-hidden').should('have.length', 3);
- cy.get('.coin_select_checkbox').eq(0).should('be.checked')
- cy.get('.coin_select_checkbox').eq(1).should('be.checked')
- cy.get('.coin_select_checkbox').eq(2).should('not.be.checked')
- cy.get('.coin_select_checkbox').eq(3).should('be.checked')
})
})
\ No newline at end of file
diff --git a/docs/performance.md b/docs/performance.md
new file mode 100644
index 000000000..ed0291a05
--- /dev/null
+++ b/docs/performance.md
@@ -0,0 +1,16 @@
+# Perfomance Hints
+
+Specter is a very flexible tool. It has several deployment models and different ways to use it. Some people use it on Node implementations like Umbrel, MyNode or RaspiBlitz. Many people use it as dedicated Electron desktop-app. Tor is also used a lot. Specter needs a Bitcoin Core node either on the same machine or on a different one. All these different usage differences do have performance impacts which might not be obvious for the average user:
+* If two components are talking via the network and are not on the same computer, that might slow down things
+* If components are talking via Tor, this will have an impact on performance
+
+So here is a small checklist which you can use to improve your performance on Specter Desktop:
+
+## Mempool.Space
+Currently (`v1.7.0`) , Mempool.Space is the default fee-estimation implementation and it's used via Tor. Changing it to Bitcoin Core in the general settings should speed up performance.
+
+# Bitcoin Core via Tor
+It's possible to connect to your Bitcoin Node via Tor. However, if you want to have access to your Specter instance from the street, perhaps rather expose Specter over Tor. If you want or have to run Bitcoin Core on a different machine than Specter, it is more advisable do that on the same network at home and not via Tor.
+
+# Tor only-mode
+Yes, we have a Tor only-mode but it's also coming with a huge performance impact. Think twice before you activate it and keep it in mind while waiting.
diff --git a/mkdocs.yml b/mkdocs.yml
index c04f241ee..345db5410 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -13,6 +13,7 @@ nav:
- Operating Guide:
- connect-your-node.md
- daemon.md
+ - performance.md
- reverse-proxy.md
- self-signed-certificates.md
- hwibridge.md
diff --git a/src/cryptoadvance/specter/config.py b/src/cryptoadvance/specter/config.py
index f3c879d26..0df9d1502 100644
--- a/src/cryptoadvance/specter/config.py
+++ b/src/cryptoadvance/specter/config.py
@@ -135,6 +135,11 @@ class BaseConfig(object):
# Babel integration. List of languages written from right to left for RTL support in the UI
RTL_LANGUAGES = ["he"]
+ # The user will get a warning if a request takes longer than this threshold
+ REQUEST_TIME_WARNING_THRESHOLD = int(
+ os.getenv("REQUEST_TIME_WARNING_THRESHOLD", "20")
+ )
+
class DevelopmentConfig(BaseConfig):
# https://stackoverflow.com/questions/22463939/demystify-flask-app-secret-key
diff --git a/src/cryptoadvance/specter/rpc.py b/src/cryptoadvance/specter/rpc.py
index 3db5e61fa..a3f01adf6 100644
--- a/src/cryptoadvance/specter/rpc.py
+++ b/src/cryptoadvance/specter/rpc.py
@@ -1,6 +1,13 @@
+import datetime
+import errno
+import json
import logging
-import requests, urllib3, json, os
-import os, sys, errno
+import os
+import sys
+
+import requests
+import urllib3
+
from .helpers import is_ip_private
from .specter_error import SpecterError
@@ -352,7 +359,7 @@ class BitcoinRPC:
url = self.url
if "wallet" in kwargs:
url = url + "/wallet/{}".format(kwargs["wallet"])
- self.trace_call(url, payload)
+ ts = self.trace_call_before(url, payload)
try:
r = self.session.post(
url, data=json.dumps(payload), headers=headers, timeout=timeout
@@ -384,6 +391,7 @@ class BitcoinRPC:
payload,
)
)
+ self.trace_call_after(url, payload, ts)
self.r = r
if r.status_code != 200:
logger.debug(f"last call FAILED: {r.text} (raising RpcError)")
@@ -394,11 +402,18 @@ class BitcoinRPC:
return r
@classmethod
- def trace_call(cls, url, payload):
- """logs out the call and its payload, reduces noise by suppressing repeated calls"""
- if False: # noise-reduction
- logger.debug(f"call({url}) payload:{payload}")
- else:
+ def trace_call_before(cls, url, payload):
+ """get a timestamp if needed in order to measure how long the call takes"""
+ if logger.level == logging.DEBUG:
+ return datetime.datetime.now()
+
+ @classmethod
+ def trace_call_after(cls, url, payload, timestamp):
+ """logs out the call and its payload (if necessary), reduces noise by suppressing repeated calls"""
+ if logger.level == logging.DEBUG:
+ timediff_ms = int(
+ (datetime.datetime.now() - timestamp).total_seconds() * 1000
+ )
current_hash = hash(
json.dumps({"url": url, "payload": payload}, sort_keys=True)
)
@@ -416,7 +431,9 @@ class BitcoinRPC:
else:
cls.last_call_hash = current_hash
logger.debug(
- "call({: <28}) payload:{}".format("/".join(url.split("/")[3:]), payload)
+ "call({: <28})({: >5}ms) payload:{}".format(
+ "/".join(url.split("/")[3:]), timediff_ms, payload
+ )
)
def __getattr__(self, method):
diff --git a/src/cryptoadvance/specter/server_endpoints/controller.py b/src/cryptoadvance/specter/server_endpoints/controller.py
index 37a90c18a..e3c014678 100644
--- a/src/cryptoadvance/specter/server_endpoints/controller.py
+++ b/src/cryptoadvance/specter/server_endpoints/controller.py
@@ -1,9 +1,10 @@
import random, traceback
+from time import time
from binascii import unhexlify
from flask import make_response
from flask_wtf.csrf import CSRFError
from werkzeug.exceptions import MethodNotAllowed
-from flask import render_template, request, redirect, url_for, flash
+from flask import render_template, request, redirect, url_for, flash, g
from flask_babel import lazy_gettext as _
from flask_login import login_required, current_user
from ..helpers import (
@@ -31,6 +32,7 @@ from .price import price_endpoint
from .settings import settings_endpoint
from .setup import setup_endpoint
from .wallets import wallets_endpoint
+from .wallets_api import wallets_endpoint_api
from ..rpc import RpcError
app.register_blueprint(auth_endpoint, url_prefix="/auth")
@@ -40,6 +42,7 @@ app.register_blueprint(price_endpoint, url_prefix="/price")
app.register_blueprint(settings_endpoint, url_prefix="/settings")
app.register_blueprint(setup_endpoint, url_prefix="/setup")
app.register_blueprint(wallets_endpoint, url_prefix="/wallets")
+app.register_blueprint(wallets_endpoint_api, url_prefix="/wallets")
rand = random.randint(0, 1e32) # to force style refresh
@@ -145,6 +148,33 @@ def selfcheck():
app.login("admin")
+@app.before_request
+def slow_request_detection():
+ """ """
+ g.start = time()
+
+
+@app.after_request
+def after_request(response):
+ diff = time() - g.start
+ if (
+ (response.response)
+ and (200 <= response.status_code < 300)
+ and (response.content_type.startswith("text/html"))
+ ):
+ threshold = app.config["REQUEST_TIME_WARNING_THRESHOLD"]
+ if diff > threshold:
+ flash(
+ _(
+ "The request before this one took {} seconds which is longer than the threshold ({}). Checkout the perfomance-improvement-hints in the documentation".format(
+ int(diff), threshold
+ )
+ ),
+ "warning",
+ )
+ return response
+
+
########## template injections #############
@app.context_processor
def inject_debug():
diff --git a/src/cryptoadvance/specter/server_endpoints/wallets.py b/src/cryptoadvance/specter/server_endpoints/wallets.py
index 1ff936a2a..cdd01e3e8 100644
--- a/src/cryptoadvance/specter/server_endpoints/wallets.py
+++ b/src/cryptoadvance/specter/server_endpoints/wallets.py
@@ -1,45 +1,24 @@
-import ast
-import base64
-import csv
import json
import logging
-import os
import random
-import time
-from binascii import b2a_base64
-from datetime import datetime
from functools import wraps
-from io import StringIO
-from math import isnan
-from numbers import Number
import requests
-from flask import Blueprint, Flask
+from cryptoadvance.specter.util.psbt_creator import PsbtCreator
+from cryptoadvance.specter.util.wallet_importer import WalletImporter
+from flask import Blueprint
from flask import current_app as app
from flask import flash, jsonify, redirect, render_template, request, url_for
from flask_babel import lazy_gettext as _
-from flask_login import current_user, login_required
-from werkzeug.wrappers import Response
-from cryptoadvance.specter.util.psbt_creator import PsbtCreator
+from flask_login import login_required
-from cryptoadvance.specter.util.wallet_importer import WalletImporter
-
-from ..helpers import (
- bcur2base64,
- get_devices_with_keys_by_type,
- get_txid,
-)
-from ..key import Key
-from ..persistence import delete_file
-from ..rpc import RpcError
-from ..specter import Specter
-from ..specter_error import SpecterError, handle_exception
-from ..util.base43 import b43_decode
-from ..util.descriptor import AddChecksum, Descriptor
-from ..util.fee_estimation import get_fees
-from ..util.price_providers import get_price_at
-from ..util.tx import decoderawtransaction
+from ..helpers import get_devices_with_keys_by_type
from ..managers.wallet_manager import purposes
+from ..persistence import delete_file
+from ..specter_error import SpecterError, handle_exception
+from ..util.fee_estimation import get_fees
+
+logger = logging.getLogger(__name__)
rand = random.randint(0, 1e32) # to force style refresh
@@ -105,6 +84,7 @@ def failed_wallets():
delete_file(fullpath.replace(".json", "_txs.csv"))
app.specter.wallet_manager.update()
except Exception as e:
+ handle_exception(e)
flash(_("Failed to delete wallet: {}").format(str(e)), "error")
return redirect("/")
@@ -122,7 +102,9 @@ def new_wallet_type():
try:
# Make sure wallet is enabled on Bitcoin Core
app.specter.rpc.listwallets()
- except Exception:
+ except Exception as e:
+ handle_exception(e)
+ # Hmm, would be better to be more precise with this exception. Best assumption:
err = _(
'
Bitcoin Core is running with wallets disabled.
Please make sure disablewallet is off (set disablewallet=0 in your bitcoin.conf), then restart Bitcoin Core and try again. See here for more information.
'
)
@@ -298,7 +280,7 @@ def new_wallet(wallet_type):
wallet_name, sigs_required, address_type, keys, cosigners
)
except Exception as e:
- app.logger.exception(e)
+ handle_exception(e)
err = _("Failed to create wallet. Error: {}").format(e)
return render_template(
"wallet/new_wallet/new_wallet_keys.jinja",
@@ -339,9 +321,7 @@ def new_wallet(wallet_type):
try:
wallet.rpc.rescanblockchain(startblock, no_wait=True)
except Exception as e:
- app.logger.error(
- "Exception while rescanning blockchain: %e" % e
- )
+ handle_exception(e)
err = "%r" % e
wallet.getdata()
return redirect(
@@ -483,12 +463,6 @@ def send_new(wallet_alias):
rbf_utxo = []
rbf_tx_id = ""
selected_coins = request.form.getlist("coinselect")
- fee_estimation = get_fees(app.specter, app.config)
- fee_estimation_data = fee_estimation.result
- if fee_estimation.error_message:
- [flash(message, "error") for message in fee_estimation.error_messages]
-
- fee_rate = fee_estimation_data["hourFee"]
if request.method == "POST":
action = request.form.get("action")
@@ -502,26 +476,16 @@ def send_new(wallet_alias):
recipients_txt=request.form["recipients"],
recipients_amount_unit=request.form.get("amount_unit_text"),
)
- try:
- psbt = psbt_creator.create_psbt(wallet)
- except SpecterError as se:
- err = str(se)
- app.logger.error(se)
- if "estimate_fee" in request.form:
- return jsonify(success=False, error=str(err))
-
- if err is None:
- if "estimate_fee" in request.form:
- return jsonify(success=True, psbt=psbt)
- return render_template(
- "wallet/send/sign/wallet_send_sign_psbt.jinja",
- psbt=psbt,
- labels=labels,
- wallet_alias=wallet_alias,
- wallet=wallet,
- specter=app.specter,
- rand=rand,
- )
+ psbt = psbt_creator.create_psbt(wallet)
+ return render_template(
+ "wallet/send/sign/wallet_send_sign_psbt.jinja",
+ psbt=psbt,
+ labels=labels,
+ wallet_alias=wallet_alias,
+ wallet=wallet,
+ specter=app.specter,
+ rand=rand,
+ )
elif action in ["rbf", "rbf_cancel"]:
try:
@@ -551,6 +515,7 @@ def send_new(wallet_alias):
rand=rand,
)
except Exception as e:
+ handle_exception(e)
flash(_("Failed to perform RBF. Error: {}").format(e), "error")
return redirect(
url_for("wallets_endpoint.history", wallet_alias=wallet_alias)
@@ -577,6 +542,7 @@ def send_new(wallet_alias):
fee_options = "manual"
rbf = True
except Exception as e:
+ handle_exception(e)
flash(_("Failed to perform RBF. Error: {}").format(e), "error")
elif action == "signhotwallet":
passphrase = request.form["passphrase"]
@@ -597,6 +563,7 @@ def send_new(wallet_alias):
signed_psbt = signed_psbt["psbt"]
psbt = current_psbt.to_dict()
except Exception as e:
+ handle_exception(e)
signed_psbt = None
flash(_("Failed to sign PSBT: {}").format(e), "error")
else:
@@ -617,13 +584,13 @@ def send_new(wallet_alias):
try:
rbf_utxo = wallet.get_rbf_utxo(rbf_tx_id)
except Exception as e:
+ handle_exception(e)
flash(_("Failed to get RBF coins. Error: {}").format(e), "error")
show_advanced_settings = (
ui_option != "ui"
or subtract
or fee_options != "dynamic"
- or fee_estimation_data["hourFee"] != fee_rate
or not rbf
or selected_coins
)
@@ -641,15 +608,12 @@ def send_new(wallet_alias):
subtract=subtract,
subtract_from=subtract_from,
fee_options=fee_options,
- fee_rate=fee_rate,
rbf=rbf,
selected_coins=selected_coins,
show_advanced_settings=show_advanced_settings,
rbf_utxo=rbf_utxo,
rbf_tx_id=rbf_tx_id,
wallet_utxo=wallet_utxo,
- fee_estimation=fee_rate,
- fee_estimation_data=fee_estimation_data,
wallet_alias=wallet_alias,
wallet=wallet,
specter=app.specter,
@@ -670,7 +634,7 @@ def send_pending(wallet_alias):
json.loads(request.form["pending_psbt"])["tx"]["txid"]
)
except Exception as e:
- app.logger.error("Could not delete Pending PSBT: %s" % e)
+ handle_exception(e)
flash(_("Could not delete Pending PSBT!"), "error")
elif action == "openpsbt":
psbt = json.loads(request.form["pending_psbt"])
@@ -703,6 +667,7 @@ def import_psbt(wallet_alias):
b64psbt = "".join(request.form["rawpsbt"].split())
psbt = wallet.importpsbt(b64psbt)
except Exception as e:
+ handle_exception(e)
flash(_("Could not import PSBT: {}").format(e), "error")
return redirect(
url_for("wallets_endpoint.import_psbt", wallet_alias=wallet_alias)
@@ -770,7 +735,7 @@ def settings(wallet_alias):
# This rpc call does not seem to return a result; use no_wait to ignore timeout errors
wallet.rpc.rescanblockchain(startblock, no_wait=True)
except Exception as e:
- app.logger.error("%s while rescanblockchain" % e)
+ handle_exception(e)
error = "%r" % e
wallet.getdata()
elif action == "abortrescan":
@@ -839,863 +804,3 @@ def settings(wallet_alias):
rand=rand,
error=error,
)
-
-
-################## Wallet util endpoints #######################
-# TODO: move these to an API endpoint
-
-
-@wallets_endpoint.route("/wallet//combine/", methods=["POST"])
-@login_required
-def combine(wallet_alias):
- wallet = app.specter.wallet_manager.get_by_alias(wallet_alias)
- # only post requests
- # FIXME: ugly...
- txid = request.form.get("txid")
- psbts = [request.form.get("psbt0").strip(), request.form.get("psbt1").strip()]
- raw = {}
- combined = None
-
- for i, psbt in enumerate(psbts):
- if not psbt:
- return _("Cannot parse empty data as PSBT"), 500
- if "UR:BYTES/" in psbt.upper():
- psbt = bcur2base64(psbt).decode()
-
- # if electrum then it's base43
- try:
- decoded = b43_decode(psbt)
- if decoded[:5] in [b"psbt\xff", b"pset\xff"]:
- psbt = b2a_base64(decoded).decode()
- else:
- psbt = decoded.hex()
- except:
- pass
-
- psbts[i] = psbt
- # psbt should start with cHNi
- # if not - maybe finalized hex tx
- if not psbt.startswith("cHNi") and not psbt.startswith("cHNl"):
- raw["hex"] = psbt
- combined = psbts[1 - i]
- # check it's hex
- try:
- bytes.fromhex(psbt)
- except:
- return _("Invalid transaction format"), 500
-
- try:
- if "hex" in raw:
- raw["complete"] = True
- raw["psbt"] = combined
- else:
- combined = app.specter.combine(psbts)
- raw = app.specter.finalize(combined)
- if "psbt" not in raw:
- raw["psbt"] = combined
- psbt = wallet.update_pending_psbt(combined, txid, raw)
- raw["devices"] = psbt["devices_signed"]
- except RpcError as e:
- return e.error_msg, e.status_code
- except Exception as e:
- return _("Unknown error: {}").format(e), 500
- return json.dumps(raw)
-
-
-@wallets_endpoint.route("/wallet//broadcast/", methods=["POST"])
-@login_required
-def broadcast(wallet_alias):
- wallet = app.specter.wallet_manager.get_by_alias(wallet_alias)
- tx = request.form.get("tx")
- res = wallet.rpc.testmempoolaccept([tx])[0]
- if res["allowed"]:
- app.specter.broadcast(tx)
- wallet.delete_spent_pending_psbts([tx])
- return jsonify(success=True)
- else:
- return jsonify(
- success=False,
- error=_(
- "Failed to broadcast transaction: transaction is invalid\n{}"
- ).format(res["reject-reason"]),
- )
-
-
-@wallets_endpoint.route(
- "/wallet//broadcast_blockexplorer/", methods=["POST"]
-)
-@login_required
-def broadcast_blockexplorer(wallet_alias):
- wallet = app.specter.wallet_manager.get_by_alias(wallet_alias)
- tx = request.form.get("tx")
- explorer = request.form.get("explorer")
- use_tor = request.form.get("use_tor", "true") == "true"
- res = wallet.rpc.testmempoolaccept([tx])[0]
- if res["allowed"]:
- try:
- if app.specter.chain == "main":
- url_network = ""
- elif app.specter.chain == "liquidv1":
- url_network = "liquid/"
- elif app.specter.chain == "test" or app.specter.chain == "testnet":
- url_network = "testnet/"
- elif app.specter.chain == "signet":
- url_network = "signet/"
- else:
- return jsonify(
- success=False,
- error=_("Failed to broadcast transaction. Network not supported."),
- )
- if explorer == "mempool":
- explorer = f"MEMPOOL_SPACE{'_ONION' if use_tor else ''}"
- elif explorer == "blockstream":
- explorer = f"BLOCKSTREAM_INFO{'_ONION' if use_tor else ''}"
- else:
- return jsonify(
- success=False,
- error=_(
- "Failed to broadcast transaction. Block explorer not supported."
- ),
- )
- requests_session = app.specter.requests_session(force_tor=use_tor)
- requests_session.post(
- f"{app.config['EXPLORERS_LIST'][explorer]['url']}{url_network}api/tx",
- data=tx,
- )
- wallet.delete_spent_pending_psbts([tx])
- return jsonify(success=True)
- except Exception as e:
- return jsonify(
- success=False,
- error=_("Failed to broadcast transaction with error: {}").format(e),
- )
- else:
- return jsonify(
- success=False,
- error=_(
- "Failed to broadcast transaction: transaction is invalid\n{}"
- ).format(res["reject-reason"]),
- )
-
-
-@wallets_endpoint.route("/wallet//decoderawtx/", methods=["GET", "POST"])
-@login_required
-@app.csrf.exempt
-def decoderawtx(wallet_alias):
- try:
- wallet = app.specter.wallet_manager.get_by_alias(wallet_alias)
- txid = request.form.get("txid", "")
- if txid:
- tx = wallet.rpc.gettransaction(txid)
- # This is a fix for Bitcoin Core versions < v0.20
- # These do not return the blockheight as part of the `gettransaction` command
- # So here we check if this property is lacking and if so
- # query the blockheader based on the transaction blockhash
- ##################### Remove from here after dropping Core v0.19 support #####################
- if "blockhash" in tx and "blockheight" not in tx:
- tx["blockheight"] = wallet.rpc.getblockheader(tx["blockhash"])["height"]
- ##################### Remove until here after dropping Core v0.19 support #####################
-
- if tx["confirmations"] == 0:
- tx["is_purged"] = wallet.is_tx_purged(txid)
- try:
- if (
- wallet.gettransaction(txid, decode=True).get("category", "")
- == "receive"
- ):
- tx["fee"] = (
- wallet.rpc.getmempoolentry(txid)["fees"]["modified"] * -1
- )
- except Exception as e:
- handle_exception(e)
- app.logger.warning(
- f"Failed to get fees from mempool entry for transaction: {txid}. Error: {e}"
- )
-
- try:
- rawtx = decoderawtransaction(tx["hex"], app.specter.chain)
- except:
- rawtx = wallet.rpc.decoderawtransaction(tx["hex"])
- # add assets
- if app.specter.is_liquid:
- for v in rawtx["vin"] + rawtx["vout"]:
- if "asset" in v:
- v["assetlabel"] = app.specter.asset_label(v["asset"])
-
- return jsonify(
- success=True,
- tx=tx,
- rawtx=rawtx,
- walletName=wallet.name,
- )
- except Exception as e:
- app.logger.warning("Failed to fetch transaction data. Exception: {}".format(e))
- return jsonify(success=False)
-
-
-@wallets_endpoint.route(
- "/wallet//rescan_progress", methods=["GET", "POST"]
-)
-@login_required
-@app.csrf.exempt
-def rescan_progress(wallet_alias):
- try:
- wallet = app.specter.wallet_manager.get_by_alias(wallet_alias)
- wallet.get_info()
- return jsonify(
- active=wallet.rescan_progress is not None,
- progress=wallet.rescan_progress,
- )
- except SpecterError as se:
- app.logger.error("SpecterError while get wallet rescan_progress: %s" % se)
- return {}
-
-
-@wallets_endpoint.route("/wallet//get_label", methods=["POST"])
-@login_required
-def get_label(wallet_alias):
- try:
- wallet = app.specter.wallet_manager.get_by_alias(wallet_alias)
- address = request.form.get("address", "")
- label = wallet.getlabel(address)
- return jsonify(
- address=address,
- label=label,
- )
- except Exception as e:
- handle_exception(e)
- return jsonify(
- success=False,
- error=_("Exception trying to get address label: Error: {}").format(e),
- )
-
-
-@wallets_endpoint.route("/wallet//set_label", methods=["POST"])
-@login_required
-def set_label(wallet_alias):
-
- wallet = app.specter.wallet_manager.get_by_alias(wallet_alias)
- address = request.form["address"]
- label = request.form["label"].rstrip()
- wallet.setlabel(address, label)
- return jsonify(success=True)
-
-
-@wallets_endpoint.route("/wallet//txlist", methods=["POST"])
-@login_required
-@app.csrf.exempt
-def txlist(wallet_alias):
- wallet = app.specter.wallet_manager.get_by_alias(wallet_alias)
- idx = int(request.form.get("idx", 0))
- limit = int(request.form.get("limit", 100))
- search = request.form.get("search", None)
- sortby = request.form.get("sortby", None)
- sortdir = request.form.get("sortdir", "asc")
- fetch_transactions = request.form.get("fetch_transactions", False)
- txlist = wallet.txlist(
- fetch_transactions=fetch_transactions,
- validate_merkle_proofs=app.specter.config.get("validate_merkle_proofs", False),
- current_blockheight=app.specter.info["blocks"],
- )
- return process_txlist(
- txlist, idx=idx, limit=limit, search=search, sortby=sortby, sortdir=sortdir
- )
-
-
-@wallets_endpoint.route("/wallet//utxo_list", methods=["POST"])
-@login_required
-@app.csrf.exempt
-def utxo_list(wallet_alias):
- wallet = app.specter.wallet_manager.get_by_alias(wallet_alias)
- idx = int(request.form.get("idx", 0))
- limit = int(request.form.get("limit", 100))
- search = request.form.get("search", None)
- sortby = request.form.get("sortby", None)
- sortdir = request.form.get("sortdir", "asc")
- txlist = wallet.full_utxo
- for tx in txlist:
- if not tx.get("label", None):
- tx["label"] = wallet.getlabel(tx["address"])
- return process_txlist(
- txlist, idx=idx, limit=limit, search=search, sortby=sortby, sortdir=sortdir
- )
-
-
-@wallets_endpoint.route("/wallets_overview/txlist", methods=["POST"])
-@login_required
-@app.csrf.exempt
-def wallets_overview_txlist():
- idx = int(request.form.get("idx", 0))
- limit = int(request.form.get("limit", 100))
- search = request.form.get("search", None)
- sortby = request.form.get("sortby", None)
- sortdir = request.form.get("sortdir", "asc")
- fetch_transactions = request.form.get("fetch_transactions", False)
- txlist = app.specter.wallet_manager.full_txlist(
- fetch_transactions=fetch_transactions,
- validate_merkle_proofs=app.specter.config.get("validate_merkle_proofs", False),
- current_blockheight=app.specter.info["blocks"],
- )
- return process_txlist(
- txlist, idx=idx, limit=limit, search=search, sortby=sortby, sortdir=sortdir
- )
-
-
-@wallets_endpoint.route("/wallets_overview/utxo_list", methods=["POST"])
-@login_required
-@app.csrf.exempt
-def wallets_overview_utxo_list():
- idx = int(request.form.get("idx", 0))
- limit = int(request.form.get("limit", 100))
- search = request.form.get("search", None)
- sortby = request.form.get("sortby", None)
- sortdir = request.form.get("sortdir", "asc")
- fetch_transactions = request.form.get("fetch_transactions", False)
- txlist = app.specter.wallet_manager.full_utxo()
- return process_txlist(
- txlist, idx=idx, limit=limit, search=search, sortby=sortby, sortdir=sortdir
- )
-
-
-@wallets_endpoint.route("/wallet//addresses_list/", methods=["POST"])
-@login_required
-@app.csrf.exempt
-def addresses_list(wallet_alias):
- """Return a JSON with keys:
- addressesList: list of addresses with the properties
- (index, address, label, used, utxo, amount)
- pageCount: total number of pages
- POST parameters:
- idx: pagination index (current page)
- limit: maximum number of items on the page
- sortby: field by which the list will be ordered
- (index, address, label, used, utxo, amount)
- sortdir: 'asc' (ascending) or 'desc' (descending) order
- addressType: the current tab address type ('receive' or 'change')"""
- wallet = app.specter.wallet_manager.get_by_alias(wallet_alias)
-
- idx = int(request.form.get("idx", 0))
- limit = int(request.form.get("limit", 100))
- sortby = request.form.get("sortby", None)
- sortdir = request.form.get("sortdir", "asc")
- address_type = request.form.get("addressType", "receive")
-
- addresses_list = wallet.addresses_info(address_type == "change")
-
- result = process_addresses_list(
- addresses_list, idx=idx, limit=limit, sortby=sortby, sortdir=sortdir
- )
-
- return jsonify(
- addressesList=json.dumps(result["addressesList"]),
- pageCount=result["pageCount"],
- )
-
-
-@wallets_endpoint.route("/wallet//addressinfo/", methods=["POST"])
-@login_required
-@app.csrf.exempt
-def addressinfo(wallet_alias):
- try:
- wallet = app.specter.wallet_manager.get_by_alias(wallet_alias)
- address = request.form.get("address", "")
- if address:
- descriptor = wallet.get_descriptor(address=address)
- address_info = wallet.get_address_info(address=address)
- return {
- "success": True,
- "address": address,
- "descriptor": descriptor,
- "walletName": wallet.name,
- "isMine": address_info and not address_info.is_external,
- **address_info,
- }
- except Exception as e:
- app.logger.warning("Failed to fetch address data. Exception: {}".format(e))
- return jsonify(success=False)
-
-
-################## Wallet CSV export data endpoints #######################
-# Export wallet addresses list
-@wallets_endpoint.route("/wallet//addresses_list.csv")
-@login_required
-def addresses_list_csv(wallet_alias):
- """Return a CSV with addresses of the containing the
- information: index, address, type, label, used, utxo and amount
- of each of them.
- GET parameters: sortby: field by which the CSV will be ordered
- (index, address, label, used, utxo, amount)
- sortdir: 'asc' (ascending) or 'desc' (descending) order
- address_type: the current tab address type ('receive' or 'change')
- onlyCurrentType: show all addresses (if false) or just the current
- type (address_type param)"""
-
- try:
- wallet = app.specter.wallet_manager.get_by_alias(wallet_alias)
-
- sortby = request.args.get("sortby", "index")
- sortdir = request.args.get("sortdir", "asc")
- address_type = request.args.get("addressType", "receive")
- only_current_type = request.args.get("onlyCurrentType", "false") == "true"
-
- if not only_current_type:
- receive_list = wallet.addresses_info(False)
- change_list = wallet.addresses_info(True)
-
- receive_result = process_addresses_list(
- receive_list, idx=0, limit=0, sortby=sortby, sortdir=sortdir
- )
-
- change_result = process_addresses_list(
- change_list, idx=0, limit=0, sortby=sortby, sortdir=sortdir
- )
-
- addressesList = (
- receive_result["addressesList"] + change_result["addressesList"]
- )
- else:
- addresses_list = wallet.addresses_info(address_type == "change")
-
- result = process_addresses_list(
- addresses_list, idx=0, limit=0, sortby=sortby, sortdir=sortdir
- )
-
- addressesList = result["addressesList"]
-
- # stream the response as the data is generated
- response = Response(
- wallet_addresses_list_to_csv(addressesList),
- mimetype="text/csv",
- )
- # add a filename
- response.headers.set(
- "Content-Disposition", "attachment", filename="addresses_list.csv"
- )
- return response
- except Exception as e:
- app.logger.error("Failed to export addresses list. Error: %s" % e)
- flash(_("Failed to export addresses list. Error: {}").format(e), "error")
- return redirect(url_for("index"))
-
-
-# Export wallet transaction history
-@wallets_endpoint.route("/wallet//transactions.csv")
-@login_required
-def tx_history_csv(wallet_alias):
- wallet = app.specter.wallet_manager.get_by_alias(wallet_alias)
- validate_merkle_proofs = app.specter.config.get("validate_merkle_proofs", False)
- txlist = wallet.txlist(validate_merkle_proofs=validate_merkle_proofs)
- search = request.args.get("search", None)
- sortby = request.args.get("sortby", "time")
- sortdir = request.args.get("sortdir", "desc")
- txlist = json.loads(
- process_txlist(
- txlist, idx=0, limit=0, search=search, sortby=sortby, sortdir=sortdir
- )["txlist"]
- )
- includePricesHistory = request.args.get("exportPrices", "false") == "true"
-
- # stream the response as the data is generated
- response = Response(
- txlist_to_csv(wallet, txlist, app.specter, current_user, includePricesHistory),
- mimetype="text/csv",
- )
- # add a filename
- response.headers.set(
- "Content-Disposition", "attachment", filename="transactions.csv"
- )
- return response
-
-
-# Export wallet UTXO list
-@wallets_endpoint.route("/wallet//utxo.csv")
-@login_required
-def utxo_csv(wallet_alias):
- try:
- wallet = app.specter.wallet_manager.get_by_alias(wallet_alias)
- includePricesHistory = request.args.get("exportPrices", "false") == "true"
- search = request.args.get("search", None)
- sortby = request.args.get("sortby", "time")
- sortdir = request.args.get("sortdir", "desc")
- txlist = json.loads(
- process_txlist(
- wallet.full_utxo,
- idx=0,
- limit=0,
- search=search,
- sortby=sortby,
- sortdir=sortdir,
- )["txlist"]
- )
- # stream the response as the data is generated
- response = Response(
- txlist_to_csv(
- wallet,
- txlist,
- app.specter,
- current_user,
- includePricesHistory,
- ),
- mimetype="text/csv",
- )
- # add a filename
- response.headers.set("Content-Disposition", "attachment", filename="utxo.csv")
- return response
- except Exception as e:
- logging.exception(e)
- return _("Failed to export wallet utxo. Error: {}").format(e), 500
-
-
-# Export all wallets transaction history combined
-@wallets_endpoint.route("/wallets_overview/full_transactions.csv")
-@login_required
-def wallet_overview_txs_csv():
- try:
- validate_merkle_proofs = app.specter.config.get("validate_merkle_proofs", False)
- txlist = app.specter.wallet_manager.full_txlist(
- validate_merkle_proofs=validate_merkle_proofs,
- )
- search = request.args.get("search", None)
- sortby = request.args.get("sortby", "time")
- sortdir = request.args.get("sortdir", "desc")
- txlist = json.loads(
- process_txlist(
- txlist, idx=0, limit=0, search=search, sortby=sortby, sortdir=sortdir
- )["txlist"]
- )
- includePricesHistory = request.args.get("exportPrices", "false") == "true"
- # stream the response as the data is generated
- response = Response(
- txlist_to_csv(
- None, txlist, app.specter, current_user, includePricesHistory
- ),
- mimetype="text/csv",
- )
- # add a filename
- response.headers.set(
- "Content-Disposition", "attachment", filename="full_transactions.csv"
- )
- return response
- except Exception as e:
- logging.exception(e)
- return _("Failed to export wallets overview history. Error: {}").format(e), 500
-
-
-# Export all wallets transaction history combined
-@wallets_endpoint.route("/wallets_overview/full_utxo.csv")
-@login_required
-def wallet_overview_utxo_csv():
- try:
- txlist = app.specter.wallet_manager.full_utxo()
- search = request.args.get("search", None)
- sortby = request.args.get("sortby", "time")
- sortdir = request.args.get("sortdir", "desc")
- txlist = json.loads(
- process_txlist(
- txlist, idx=0, limit=0, search=search, sortby=sortby, sortdir=sortdir
- )["txlist"]
- )
- includePricesHistory = request.args.get("exportPrices", "false") == "true"
- # stream the response as the data is generated
- response = Response(
- txlist_to_csv(
- None, txlist, app.specter, current_user, includePricesHistory
- ),
- mimetype="text/csv",
- )
- # add a filename
- response.headers.set(
- "Content-Disposition", "attachment", filename="full_utxo.csv"
- )
- return response
- except Exception as e:
- logging.exception(e)
- return _("Failed to export wallets overview utxo. Error: {}").format(e), 500
-
-
-################## Helpers #######################
-
-# Transactions list to user-friendly CSV format
-def txlist_to_csv(wallet, _txlist, specter, current_user, includePricesHistory=False):
- txlist = []
- for tx in _txlist:
- if isinstance(tx["address"], list):
- _tx = tx.copy()
- for i in range(0, len(tx["address"])):
- _tx["address"] = tx["address"][i]
- _tx["amount"] = tx["amount"][i]
- txlist.append(_tx.copy())
- else:
- txlist.append(tx.copy())
- data = StringIO()
- w = csv.writer(data)
- # write header
- symbol = "USD"
- if specter.price_provider.endswith("_eur"):
- symbol = "EUR"
- elif specter.price_provider.endswith("_gbp"):
- symbol = "GBP"
- row = (
- _("Date"),
- _("Label"),
- _("Category"),
- _("Amount ({})").format(specter.unit.upper()),
- _("Value ({})").format(symbol),
- _("Rate (BTC/{})").format(symbol)
- if specter.unit != "sat"
- else _("Rate ({}/SAT)").format(symbol),
- _("TxID"),
- _("Address"),
- _("Block Height"),
- _("Timestamp"),
- )
- if not wallet:
- row = (_("Wallet"),) + row
- w.writerow(row)
- yield data.getvalue()
- data.seek(0)
- data.truncate(0)
-
- # write each log item
- _wallet = wallet
- for tx in txlist:
- if not wallet:
- wallet_alias = tx.get("wallet_alias", None)
- try:
- _wallet = specter.wallet_manager.get_by_alias(wallet_alias)
- except Exception as e:
- continue
- label = _wallet.getlabel(tx["address"])
- if label == tx["address"]:
- label = ""
- tx_raw = _wallet.gettransaction(tx["txid"])
- if not tx.get("blockheight", None):
- if tx_raw.get("blockheight", None):
- tx["blockheight"] = tx_raw["blockheight"]
- else:
- tx["blockheight"] = "Unconfirmed"
- if specter.unit == "sat":
- value = float(tx["amount"])
- tx["amount"] = round(value * 1e8)
- if includePricesHistory:
- success, rate, symbol = get_price_at(
- specter, current_user, timestamp=tx["time"]
- )
- else:
- success = False
- if success:
- rate = float(rate)
- if specter.unit == "sat":
- rate = rate / 1e8
- amount_price = float(tx["amount"]) * rate
- if specter.unit == "sat":
- rate = round(1 / rate)
- else:
- amount_price = None
- rate = "-"
-
- row = (
- time.strftime("%Y-%m-%d", time.localtime(tx["time"])),
- label,
- tx["category"],
- round(tx["amount"], (0 if specter.unit == "sat" else 8)),
- round(amount_price * 100) / 100 if amount_price is not None else "-",
- rate,
- tx["txid"],
- tx["address"],
- tx["blockheight"],
- tx["time"],
- )
- if not wallet:
- row = (tx.get("wallet_alias", ""),) + row
- w.writerow(row)
- yield data.getvalue()
- data.seek(0)
- data.truncate(0)
-
-
-# Addresses list to user-friendly CSV format
-def addresses_list_to_csv(wallet):
- data = StringIO()
- w = csv.writer(data)
- # write header
- row = (
- _("Address"),
- _("Label"),
- _("Index"),
- _("Used"),
- _("Current balance"),
- )
- w.writerow(row)
- yield data.getvalue()
- data.seek(0)
- data.truncate(0)
-
- # write each log item
- for address in wallet._addresses:
- address_info = wallet.get_address_info(address)
- if not address_info.is_labeled and not address_info.used:
- continue
- row = (
- address,
- address_info.label,
- _("(external)")
- if address_info.is_external
- else (
- str(address_info.index)
- + (_(" (change)") if address_info.change else "")
- ),
- address_info.used,
- )
- if address_info.is_external:
- balance_on_address = _("unknown (external address)")
- else:
- balance_on_address = 0
- if address_info.used:
- for tx in wallet.full_utxo:
- if tx.get("address", "") == address:
- balance_on_address += tx.get("amount", 0)
- row += (balance_on_address,)
-
- w.writerow(row)
- yield data.getvalue()
- data.seek(0)
- data.truncate(0)
-
-
-def wallet_addresses_list_to_csv(addresses_list):
- """Convert a list of the wallet addresses to user-friendly CSV format
- Parameters: addresses_list: a dict of addresses informations
- (index, address, type, label, used, utxo and amount)"""
- data = StringIO()
- w = csv.writer(data)
- # write header
- row = (
- _("Index"),
- _("Address"),
- _("Type"),
- _("Label"),
- _("Used"),
- _("UTXO"),
- _("Amount (BTC)"),
- )
- w.writerow(row)
- yield data.getvalue()
- data.seek(0)
- data.truncate(0)
-
- # write each log item
- for address_item in addresses_list:
-
- used = "Yes" if address_item["used"] else "No"
-
- row = (
- address_item["index"],
- address_item["address"],
- address_item["type"],
- address_item["label"],
- used,
- address_item["utxo"],
- address_item["amount"],
- )
-
- w.writerow(row)
- yield data.getvalue()
- data.seek(0)
- data.truncate(0)
-
-
-def process_txlist(txlist, idx=0, limit=100, search=None, sortby=None, sortdir="asc"):
- if search:
- txlist = [
- tx
- for tx in txlist
- if search in tx["txid"]
- or (
- any(search in address for address in tx["address"])
- if isinstance(tx["address"], list)
- else search in tx["address"]
- )
- or (
- any(search in label for label in tx.get("label", ""))
- if isinstance(tx.get("label", ""), list)
- else search in tx.get("label", "")
- )
- or (
- any(search in str(amount) for amount in tx["amount"])
- if isinstance(tx["amount"], list)
- else search in str(tx["amount"])
- )
- or search in str(tx["confirmations"])
- or search in str(tx["time"])
- or search
- in str(format(datetime.fromtimestamp(tx["time"]), "%d.%m.%Y %H:%M"))
- ]
- if sortby:
-
- def sort(tx):
- val = tx.get(sortby, None)
- final = val
- if val:
- if isinstance(val, list):
- if isinstance(val[0], Number):
- final = sum(val)
- elif isinstance(val[0], str):
- final = sorted(
- val, key=lambda s: s.lower(), reverse=sortdir != "asc"
- )[0].lower()
- elif isinstance(val, str):
- final = val.lower()
- return final
-
- txlist = sorted(txlist, key=sort, reverse=sortdir != "asc")
- if limit:
- page_count = (len(txlist) // limit) + (0 if len(txlist) % limit == 0 else 1)
- txlist = txlist[limit * idx : limit * (idx + 1)]
- else:
- page_count = 1
- # add assets
- if app.specter.is_liquid:
- for tx in txlist:
- if "asset" in tx:
- if isinstance(tx["asset"], list):
- tx["assetlabel"] = [
- app.specter.asset_label(asset) for asset in tx["asset"]
- ]
- else:
- tx["assetlabel"] = app.specter.asset_label(tx["asset"])
- return {"txlist": json.dumps(txlist), "pageCount": page_count}
-
-
-def process_addresses_list(
- addresses_list, idx=0, limit=100, sortby=None, sortdir="asc"
-):
- """Receive an address list as parameter and sort it or slice it for pagination.
- Parameters: addresses_list: list of dict with the keys
- (index, address, label, used, utxo, amount)
- idx: pagination index (current page)
- limit: maximum number of items on the page
- sortby: field by which the list will be ordered
- (index, address, label, used, utxo, amount)
- sortdir: 'asc' (ascending) or 'desc' (descending) order"""
- if sortby:
-
- def sort(addr):
- val = addr.get(sortby, None)
- final = val
- if val:
- if isinstance(val, str):
- final = val.lower()
- return final
-
- addresses_list = sorted(addresses_list, key=sort, reverse=sortdir != "asc")
-
- if limit:
- page_count = (len(addresses_list) // limit) + (
- 0 if len(addresses_list) % limit == 0 else 1
- )
- addresses_list = addresses_list[limit * idx : limit * (idx + 1)]
- else:
- page_count = 1
-
- return {"addressesList": addresses_list, "pageCount": page_count}
diff --git a/src/cryptoadvance/specter/server_endpoints/wallets_api.py b/src/cryptoadvance/specter/server_endpoints/wallets_api.py
new file mode 100644
index 000000000..373edfa15
--- /dev/null
+++ b/src/cryptoadvance/specter/server_endpoints/wallets_api.py
@@ -0,0 +1,968 @@
+import csv
+import json
+import logging
+import time
+from binascii import b2a_base64
+from datetime import datetime
+from io import StringIO
+from math import isnan
+from numbers import Number
+
+import requests
+from flask import Blueprint
+from flask import current_app as app
+from flask import flash, jsonify, redirect, request, url_for
+from flask_babel import lazy_gettext as _
+from flask_login import current_user, login_required
+from werkzeug.wrappers import Response
+
+from cryptoadvance.specter.util.psbt_creator import PsbtCreator
+
+from ..helpers import bcur2base64
+from ..rpc import RpcError
+from ..server_endpoints.filters import assetlabel
+from ..specter_error import SpecterError, handle_exception
+from ..util.base43 import b43_decode
+from ..util.descriptor import Descriptor
+from ..util.fee_estimation import FeeEstimationResultEncoder, get_fees
+from ..util.price_providers import get_price_at
+from ..util.tx import decoderawtransaction
+
+logger = logging.getLogger(__name__)
+
+wallets_endpoint_api = Blueprint("wallets_endpoint_api", __name__)
+
+
+@wallets_endpoint_api.route("/fees", methods=["GET"])
+@login_required
+def fees():
+ return json.dumps(get_fees(app.specter, app.config), cls=FeeEstimationResultEncoder)
+
+
+@wallets_endpoint_api.route("/wallet//combine/", methods=["POST"])
+@login_required
+def combine(wallet_alias):
+ wallet = app.specter.wallet_manager.get_by_alias(wallet_alias)
+ # only post requests
+ # FIXME: ugly...
+ txid = request.form.get("txid")
+ psbts = [request.form.get("psbt0").strip(), request.form.get("psbt1").strip()]
+ raw = {}
+ combined = None
+
+ for i, psbt in enumerate(psbts):
+ if not psbt:
+ return _("Cannot parse empty data as PSBT"), 500
+ if "UR:BYTES/" in psbt.upper():
+ psbt = bcur2base64(psbt).decode()
+
+ # if electrum then it's base43
+ try:
+ decoded = b43_decode(psbt)
+ if decoded[:5] in [b"psbt\xff", b"pset\xff"]:
+ psbt = b2a_base64(decoded).decode()
+ else:
+ psbt = decoded.hex()
+ except:
+ pass
+
+ psbts[i] = psbt
+ # psbt should start with cHNi
+ # if not - maybe finalized hex tx
+ if not psbt.startswith("cHNi") and not psbt.startswith("cHNl"):
+ raw["hex"] = psbt
+ combined = psbts[1 - i]
+ # check it's hex
+ try:
+ bytes.fromhex(psbt)
+ except:
+ return _("Invalid transaction format"), 500
+
+ try:
+ if "hex" in raw:
+ raw["complete"] = True
+ raw["psbt"] = combined
+ else:
+ combined = app.specter.combine(psbts)
+ raw = app.specter.finalize(combined)
+ if "psbt" not in raw:
+ raw["psbt"] = combined
+ psbt = wallet.update_pending_psbt(combined, txid, raw)
+ raw["devices"] = psbt["devices_signed"]
+ except RpcError as e:
+ return e.error_msg, e.status_code
+ except Exception as e:
+ handle_exception(e)
+ return _("Unknown error: {}").format(e), 500
+ return json.dumps(raw)
+
+
+@wallets_endpoint_api.route("/wallet//broadcast/", methods=["POST"])
+@login_required
+def broadcast(wallet_alias):
+ wallet = app.specter.wallet_manager.get_by_alias(wallet_alias)
+ tx = request.form.get("tx")
+ res = wallet.rpc.testmempoolaccept([tx])[0]
+ if res["allowed"]:
+ app.specter.broadcast(tx)
+ wallet.delete_spent_pending_psbts([tx])
+ return jsonify(success=True)
+ else:
+ return jsonify(
+ success=False,
+ error=_(
+ "Failed to broadcast transaction: transaction is invalid\n{}"
+ ).format(res["reject-reason"]),
+ )
+
+
+@wallets_endpoint_api.route(
+ "/wallet//broadcast_blockexplorer/", methods=["POST"]
+)
+@login_required
+def broadcast_blockexplorer(wallet_alias):
+ wallet = app.specter.wallet_manager.get_by_alias(wallet_alias)
+ tx = request.form.get("tx")
+ explorer = request.form.get("explorer")
+ use_tor = request.form.get("use_tor", "true") == "true"
+ res = wallet.rpc.testmempoolaccept([tx])[0]
+ if res["allowed"]:
+ try:
+ if app.specter.chain == "main":
+ url_network = ""
+ elif app.specter.chain == "liquidv1":
+ url_network = "liquid/"
+ elif app.specter.chain == "test" or app.specter.chain == "testnet":
+ url_network = "testnet/"
+ elif app.specter.chain == "signet":
+ url_network = "signet/"
+ else:
+ return jsonify(
+ success=False,
+ error=_("Failed to broadcast transaction. Network not supported."),
+ )
+ if explorer == "mempool":
+ explorer = f"MEMPOOL_SPACE{'_ONION' if use_tor else ''}"
+ elif explorer == "blockstream":
+ explorer = f"BLOCKSTREAM_INFO{'_ONION' if use_tor else ''}"
+ else:
+ return jsonify(
+ success=False,
+ error=_(
+ "Failed to broadcast transaction. Block explorer not supported."
+ ),
+ )
+ requests_session = app.specter.requests_session(force_tor=use_tor)
+ requests_session.post(
+ f"{app.config['EXPLORERS_LIST'][explorer]['url']}{url_network}api/tx",
+ data=tx,
+ )
+ wallet.delete_spent_pending_psbts([tx])
+ return jsonify(success=True)
+ except Exception as e:
+ handle_exception(e)
+ return jsonify(
+ success=False,
+ error=_("Failed to broadcast transaction with error: {}").format(e),
+ )
+ else:
+ return jsonify(
+ success=False,
+ error=_(
+ "Failed to broadcast transaction: transaction is invalid\n{}"
+ ).format(res["reject-reason"]),
+ )
+
+
+@wallets_endpoint_api.route(
+ "/wallet//decoderawtx/", methods=["GET", "POST"]
+)
+@login_required
+@app.csrf.exempt
+def decoderawtx(wallet_alias):
+ try:
+ wallet = app.specter.wallet_manager.get_by_alias(wallet_alias)
+ txid = request.form.get("txid", "")
+ if txid:
+ tx = wallet.rpc.gettransaction(txid)
+ # This is a fix for Bitcoin Core versions < v0.20
+ # These do not return the blockheight as part of the `gettransaction` command
+ # So here we check if this property is lacking and if so
+ # query the blockheader based on the transaction blockhash
+ ##################### Remove from here after dropping Core v0.19 support #####################
+ if "blockhash" in tx and "blockheight" not in tx:
+ tx["blockheight"] = wallet.rpc.getblockheader(tx["blockhash"])["height"]
+ ##################### Remove until here after dropping Core v0.19 support #####################
+
+ if tx["confirmations"] == 0:
+ tx["is_purged"] = wallet.is_tx_purged(txid)
+ try:
+ if (
+ wallet.gettransaction(txid, decode=True).get("category", "")
+ == "receive"
+ ):
+ tx["fee"] = (
+ wallet.rpc.getmempoolentry(txid)["fees"]["modified"] * -1
+ )
+ except Exception as e:
+ handle_exception(e)
+ app.logger.warning(
+ f"Failed to get fees from mempool entry for transaction: {txid}. Error: {e}"
+ )
+
+ try:
+ rawtx = decoderawtransaction(tx["hex"], app.specter.chain)
+ except:
+ rawtx = wallet.rpc.decoderawtransaction(tx["hex"])
+ # add assets
+ if app.specter.is_liquid:
+ for v in rawtx["vin"] + rawtx["vout"]:
+ if "asset" in v:
+ v["assetlabel"] = app.specter.asset_label(v["asset"])
+
+ return jsonify(
+ success=True,
+ tx=tx,
+ rawtx=rawtx,
+ walletName=wallet.name,
+ )
+ except Exception as e:
+ handle_exception(e)
+ return jsonify(success=False)
+
+
+@wallets_endpoint_api.route(
+ "/wallet//rescan_progress", methods=["GET", "POST"]
+)
+@login_required
+@app.csrf.exempt
+def rescan_progress(wallet_alias):
+ try:
+ wallet = app.specter.wallet_manager.get_by_alias(wallet_alias)
+ wallet.get_info()
+ return jsonify(
+ active=wallet.rescan_progress is not None,
+ progress=wallet.rescan_progress,
+ )
+ except SpecterError as se:
+ app.logger.error("SpecterError while get wallet rescan_progress: %s" % se)
+ return {}
+
+
+@wallets_endpoint_api.route("/wallet//get_label", methods=["POST"])
+@login_required
+def get_label(wallet_alias):
+ try:
+ wallet = app.specter.wallet_manager.get_by_alias(wallet_alias)
+ address = request.form.get("address", "")
+ label = wallet.getlabel(address)
+ return jsonify(
+ address=address,
+ label=label,
+ )
+ except Exception as e:
+ handle_exception(e)
+ return jsonify(
+ success=False,
+ error=_("Exception trying to get address label: Error: {}").format(e),
+ )
+
+
+@wallets_endpoint_api.route("/wallet//set_label", methods=["POST"])
+@login_required
+def set_label(wallet_alias):
+
+ wallet = app.specter.wallet_manager.get_by_alias(wallet_alias)
+ address = request.form["address"]
+ label = request.form["label"].rstrip()
+ wallet.setlabel(address, label)
+ return jsonify(success=True)
+
+
+@wallets_endpoint_api.route("/wallet//txlist", methods=["POST"])
+@login_required
+@app.csrf.exempt
+def txlist(wallet_alias):
+ wallet = app.specter.wallet_manager.get_by_alias(wallet_alias)
+ idx = int(request.form.get("idx", 0))
+ limit = int(request.form.get("limit", 100))
+ search = request.form.get("search", None)
+ sortby = request.form.get("sortby", None)
+ sortdir = request.form.get("sortdir", "asc")
+ fetch_transactions = request.form.get("fetch_transactions", False)
+ txlist = wallet.txlist(
+ fetch_transactions=fetch_transactions,
+ validate_merkle_proofs=app.specter.config.get("validate_merkle_proofs", False),
+ current_blockheight=app.specter.info["blocks"],
+ )
+ return process_txlist(
+ txlist, idx=idx, limit=limit, search=search, sortby=sortby, sortdir=sortdir
+ )
+
+
+@wallets_endpoint_api.route("/wallet//utxo_list", methods=["POST"])
+@login_required
+@app.csrf.exempt
+def utxo_list(wallet_alias):
+ wallet = app.specter.wallet_manager.get_by_alias(wallet_alias)
+ idx = int(request.form.get("idx", 0))
+ limit = int(request.form.get("limit", 100))
+ search = request.form.get("search", None)
+ sortby = request.form.get("sortby", None)
+ sortdir = request.form.get("sortdir", "asc")
+ txlist = wallet.full_utxo
+ for tx in txlist:
+ if not tx.get("label", None):
+ tx["label"] = wallet.getlabel(tx["address"])
+ return process_txlist(
+ txlist, idx=idx, limit=limit, search=search, sortby=sortby, sortdir=sortdir
+ )
+
+
+@wallets_endpoint_api.route("/wallets_overview/txlist", methods=["POST"])
+@login_required
+@app.csrf.exempt
+def wallets_overview_txlist():
+ idx = int(request.form.get("idx", 0))
+ limit = int(request.form.get("limit", 100))
+ search = request.form.get("search", None)
+ sortby = request.form.get("sortby", None)
+ sortdir = request.form.get("sortdir", "asc")
+ fetch_transactions = request.form.get("fetch_transactions", False)
+ txlist = app.specter.wallet_manager.full_txlist(
+ fetch_transactions=fetch_transactions,
+ validate_merkle_proofs=app.specter.config.get("validate_merkle_proofs", False),
+ current_blockheight=app.specter.info["blocks"],
+ )
+ return process_txlist(
+ txlist, idx=idx, limit=limit, search=search, sortby=sortby, sortdir=sortdir
+ )
+
+
+@wallets_endpoint_api.route("/wallets_overview/utxo_list", methods=["POST"])
+@login_required
+@app.csrf.exempt
+def wallets_overview_utxo_list():
+ idx = int(request.form.get("idx", 0))
+ limit = int(request.form.get("limit", 100))
+ search = request.form.get("search", None)
+ sortby = request.form.get("sortby", None)
+ sortdir = request.form.get("sortdir", "asc")
+ fetch_transactions = request.form.get("fetch_transactions", False)
+ txlist = app.specter.wallet_manager.full_utxo()
+ return process_txlist(
+ txlist, idx=idx, limit=limit, search=search, sortby=sortby, sortdir=sortdir
+ )
+
+
+@wallets_endpoint_api.route("/wallet//addresses_list/", methods=["POST"])
+@login_required
+@app.csrf.exempt
+def addresses_list(wallet_alias):
+ """Return a JSON with keys:
+ addressesList: list of addresses with the properties
+ (index, address, label, used, utxo, amount)
+ pageCount: total number of pages
+ POST parameters:
+ idx: pagination index (current page)
+ limit: maximum number of items on the page
+ sortby: field by which the list will be ordered
+ (index, address, label, used, utxo, amount)
+ sortdir: 'asc' (ascending) or 'desc' (descending) order
+ addressType: the current tab address type ('receive' or 'change')"""
+ wallet = app.specter.wallet_manager.get_by_alias(wallet_alias)
+
+ idx = int(request.form.get("idx", 0))
+ limit = int(request.form.get("limit", 100))
+ sortby = request.form.get("sortby", None)
+ sortdir = request.form.get("sortdir", "asc")
+ address_type = request.form.get("addressType", "receive")
+
+ addresses_list = wallet.addresses_info(address_type == "change")
+
+ result = process_addresses_list(
+ addresses_list, idx=idx, limit=limit, sortby=sortby, sortdir=sortdir
+ )
+
+ return jsonify(
+ addressesList=json.dumps(result["addressesList"]),
+ pageCount=result["pageCount"],
+ )
+
+
+@wallets_endpoint_api.route("/wallet//addressinfo/", methods=["POST"])
+@login_required
+@app.csrf.exempt
+def addressinfo(wallet_alias):
+ try:
+ wallet = app.specter.wallet_manager.get_by_alias(wallet_alias)
+ address = request.form.get("address", "")
+ if address:
+ descriptor = wallet.get_descriptor(address=address)
+ address_info = wallet.get_address_info(address=address)
+ return {
+ "success": True,
+ "address": address,
+ "descriptor": descriptor,
+ "walletName": wallet.name,
+ "isMine": address_info and not address_info.is_external,
+ **address_info,
+ }
+ except Exception as e:
+ handle_exception(e)
+ return jsonify(success=False)
+
+
+################## Wallet CSV export data endpoints #######################
+# Export wallet addresses list
+@wallets_endpoint_api.route("/wallet//addresses_list.csv")
+@login_required
+def addresses_list_csv(wallet_alias):
+ """Return a CSV with addresses of the containing the
+ information: index, address, type, label, used, utxo and amount
+ of each of them.
+ GET parameters: sortby: field by which the CSV will be ordered
+ (index, address, label, used, utxo, amount)
+ sortdir: 'asc' (ascending) or 'desc' (descending) order
+ address_type: the current tab address type ('receive' or 'change')
+ onlyCurrentType: show all addresses (if false) or just the current
+ type (address_type param)"""
+
+ try:
+ wallet = app.specter.wallet_manager.get_by_alias(wallet_alias)
+
+ sortby = request.args.get("sortby", "index")
+ sortdir = request.args.get("sortdir", "asc")
+ address_type = request.args.get("addressType", "receive")
+ only_current_type = request.args.get("onlyCurrentType", "false") == "true"
+
+ if not only_current_type:
+ receive_list = wallet.addresses_info(False)
+ change_list = wallet.addresses_info(True)
+
+ receive_result = process_addresses_list(
+ receive_list, idx=0, limit=0, sortby=sortby, sortdir=sortdir
+ )
+
+ change_result = process_addresses_list(
+ change_list, idx=0, limit=0, sortby=sortby, sortdir=sortdir
+ )
+
+ addressesList = (
+ receive_result["addressesList"] + change_result["addressesList"]
+ )
+ else:
+ addresses_list = wallet.addresses_info(address_type == "change")
+
+ result = process_addresses_list(
+ addresses_list, idx=0, limit=0, sortby=sortby, sortdir=sortdir
+ )
+
+ addressesList = result["addressesList"]
+
+ # stream the response as the data is generated
+ response = Response(
+ wallet_addresses_list_to_csv(addressesList),
+ mimetype="text/csv",
+ )
+ # add a filename
+ response.headers.set(
+ "Content-Disposition", "attachment", filename="addresses_list.csv"
+ )
+ return response
+ except Exception as e:
+ handle_exception(e)
+ flash(_("Failed to export addresses list. Error: {}").format(e), "error")
+ return redirect(url_for("index"))
+
+
+# Export wallet transaction history
+@wallets_endpoint_api.route("/wallet//transactions.csv")
+@login_required
+def tx_history_csv(wallet_alias):
+ wallet = app.specter.wallet_manager.get_by_alias(wallet_alias)
+ validate_merkle_proofs = app.specter.config.get("validate_merkle_proofs", False)
+ txlist = wallet.txlist(validate_merkle_proofs=validate_merkle_proofs)
+ search = request.args.get("search", None)
+ sortby = request.args.get("sortby", "time")
+ sortdir = request.args.get("sortdir", "desc")
+ txlist = json.loads(
+ process_txlist(
+ txlist, idx=0, limit=0, search=search, sortby=sortby, sortdir=sortdir
+ )["txlist"]
+ )
+ includePricesHistory = request.args.get("exportPrices", "false") == "true"
+
+ # stream the response as the data is generated
+ response = Response(
+ txlist_to_csv(wallet, txlist, app.specter, current_user, includePricesHistory),
+ mimetype="text/csv",
+ )
+ # add a filename
+ response.headers.set(
+ "Content-Disposition", "attachment", filename="transactions.csv"
+ )
+ return response
+
+
+# Export wallet UTXO list
+@wallets_endpoint_api.route("/wallet//utxo.csv")
+@login_required
+def utxo_csv(wallet_alias):
+ try:
+ wallet = app.specter.wallet_manager.get_by_alias(wallet_alias)
+ includePricesHistory = request.args.get("exportPrices", "false") == "true"
+ search = request.args.get("search", None)
+ sortby = request.args.get("sortby", "time")
+ sortdir = request.args.get("sortdir", "desc")
+ txlist = json.loads(
+ process_txlist(
+ wallet.full_utxo,
+ idx=0,
+ limit=0,
+ search=search,
+ sortby=sortby,
+ sortdir=sortdir,
+ )["txlist"]
+ )
+ # stream the response as the data is generated
+ response = Response(
+ txlist_to_csv(
+ wallet,
+ txlist,
+ app.specter,
+ current_user,
+ includePricesHistory,
+ ),
+ mimetype="text/csv",
+ )
+ # add a filename
+ response.headers.set("Content-Disposition", "attachment", filename="utxo.csv")
+ return response
+ except Exception as e:
+ handle_exception(e)
+ return _("Failed to export wallet utxo. Error: {}").format(e), 500
+
+
+@wallets_endpoint_api.route("/wallet//send/estimatefee", methods=["POST"])
+@login_required
+def estimate_fee(wallet_alias):
+ """Returns a json-representation of a psbt which did not get persisted. Kind of a draft-run."""
+ wallet = app.specter.wallet_manager.get_by_alias(wallet_alias)
+ # update balances in the wallet
+ wallet.update_balance()
+ # update utxo list for coin selection
+ wallet.check_utxo()
+ if request.form.get("estimate_fee") != "true":
+ # Very critical as this form-value will prevent persisting the PSBT
+ return jsonify(
+ success=False,
+ error="Your Form did not specify estimate_fee = false. This call is not allowed",
+ )
+ psbt_creator = PsbtCreator(
+ app.specter,
+ wallet,
+ request.form.get("ui_option", "ui"),
+ request_form=request.form,
+ recipients_txt=request.form["recipients"],
+ recipients_amount_unit=request.form.get("amount_unit_text"),
+ )
+ try:
+ # Won't get persisted
+ psbt = psbt_creator.create_psbt(wallet)
+ return jsonify(success=True, psbt=psbt)
+ except SpecterError as se:
+ app.logger.error(se)
+ return jsonify(success=False, error=str(se))
+
+
+@wallets_endpoint_api.route("/wallet//asset_balances")
+@login_required
+def asset_balances(wallet_alias):
+ try:
+ wallet = app.specter.wallet_manager.get_by_alias(wallet_alias)
+ if app.specter.is_testnet:
+ label = "tBTC"
+ elif app.specter.is_liquid:
+ label = "LBTC"
+ else:
+ label = "BTC"
+
+ amounts = []
+ textUnit = app.specter.unit
+ asset_balances = {
+ "btc": {
+ "balance": wallet.full_available_balance,
+ "label": label,
+ },
+ "sat": {
+ "balance": int(wallet.full_available_balance * 1e8),
+ "label": "sat",
+ },
+ }
+ for asset in wallet.balance.get("assets", {}).keys():
+ asset_balances["asset"] = {
+ "balance": wallet.balance.get("assets", {})
+ .get(asset, {})
+ .get("trusted", 0),
+ "label": assetlabel(None, asset),
+ }
+ return asset_balances
+ except Exception as e:
+ handle_exception(e)
+ return _("Failed to list asses_balances. Error: {}").format(e), 500
+
+
+# Export all wallets transaction history combined
+@wallets_endpoint_api.route("/wallets_overview/full_transactions.csv")
+@login_required
+def wallet_overview_txs_csv():
+ try:
+ validate_merkle_proofs = app.specter.config.get("validate_merkle_proofs", False)
+ txlist = app.specter.wallet_manager.full_txlist(
+ validate_merkle_proofs=validate_merkle_proofs,
+ )
+ search = request.args.get("search", None)
+ sortby = request.args.get("sortby", "time")
+ sortdir = request.args.get("sortdir", "desc")
+ txlist = json.loads(
+ process_txlist(
+ txlist, idx=0, limit=0, search=search, sortby=sortby, sortdir=sortdir
+ )["txlist"]
+ )
+ includePricesHistory = request.args.get("exportPrices", "false") == "true"
+ # stream the response as the data is generated
+ response = Response(
+ txlist_to_csv(
+ None, txlist, app.specter, current_user, includePricesHistory
+ ),
+ mimetype="text/csv",
+ )
+ # add a filename
+ response.headers.set(
+ "Content-Disposition", "attachment", filename="full_transactions.csv"
+ )
+ return response
+ except Exception as e:
+ handle_exception(e)
+ return _("Failed to export wallets overview history. Error: {}").format(e), 500
+
+
+# Export all wallets transaction history combined
+@wallets_endpoint_api.route("/wallets_overview/full_utxo.csv")
+@login_required
+def wallet_overview_utxo_csv():
+ try:
+ txlist = app.specter.wallet_manager.full_utxo()
+ search = request.args.get("search", None)
+ sortby = request.args.get("sortby", "time")
+ sortdir = request.args.get("sortdir", "desc")
+ txlist = json.loads(
+ process_txlist(
+ txlist, idx=0, limit=0, search=search, sortby=sortby, sortdir=sortdir
+ )["txlist"]
+ )
+ includePricesHistory = request.args.get("exportPrices", "false") == "true"
+ # stream the response as the data is generated
+ response = Response(
+ txlist_to_csv(
+ None, txlist, app.specter, current_user, includePricesHistory
+ ),
+ mimetype="text/csv",
+ )
+ # add a filename
+ response.headers.set(
+ "Content-Disposition", "attachment", filename="full_utxo.csv"
+ )
+ return response
+ except Exception as e:
+ handle_exception(e)
+ return _("Failed to export wallets overview utxo. Error: {}").format(e), 500
+
+
+################## Helpers #######################
+
+# Transactions list to user-friendly CSV format
+def txlist_to_csv(wallet, _txlist, specter, current_user, includePricesHistory=False):
+ txlist = []
+ for tx in _txlist:
+ if isinstance(tx["address"], list):
+ _tx = tx.copy()
+ for i in range(0, len(tx["address"])):
+ _tx["address"] = tx["address"][i]
+ _tx["amount"] = tx["amount"][i]
+ txlist.append(_tx.copy())
+ else:
+ txlist.append(tx.copy())
+ data = StringIO()
+ w = csv.writer(data)
+ # write header
+ symbol = "USD"
+ if specter.price_provider.endswith("_eur"):
+ symbol = "EUR"
+ elif specter.price_provider.endswith("_gbp"):
+ symbol = "GBP"
+ row = (
+ _("Date"),
+ _("Label"),
+ _("Category"),
+ _("Amount ({})").format(specter.unit.upper()),
+ _("Value ({})").format(symbol),
+ _("Rate (BTC/{})").format(symbol)
+ if specter.unit != "sat"
+ else _("Rate ({}/SAT)").format(symbol),
+ _("TxID"),
+ _("Address"),
+ _("Block Height"),
+ _("Timestamp"),
+ )
+ if not wallet:
+ row = (_("Wallet"),) + row
+ w.writerow(row)
+ yield data.getvalue()
+ data.seek(0)
+ data.truncate(0)
+
+ # write each log item
+ _wallet = wallet
+ for tx in txlist:
+ if not wallet:
+ wallet_alias = tx.get("wallet_alias", None)
+ try:
+ _wallet = specter.wallet_manager.get_by_alias(wallet_alias)
+ except Exception as e:
+ continue
+ label = _wallet.getlabel(tx["address"])
+ if label == tx["address"]:
+ label = ""
+ tx_raw = _wallet.gettransaction(tx["txid"])
+ if not tx.get("blockheight", None):
+ if tx_raw.get("blockheight", None):
+ tx["blockheight"] = tx_raw["blockheight"]
+ else:
+ tx["blockheight"] = "Unconfirmed"
+ if specter.unit == "sat":
+ value = float(tx["amount"])
+ tx["amount"] = round(value * 1e8)
+ if includePricesHistory:
+ success, rate, symbol = get_price_at(
+ specter, current_user, timestamp=tx["time"]
+ )
+ else:
+ success = False
+ if success:
+ rate = float(rate)
+ if specter.unit == "sat":
+ rate = rate / 1e8
+ amount_price = float(tx["amount"]) * rate
+ if specter.unit == "sat":
+ rate = round(1 / rate)
+ else:
+ amount_price = None
+ rate = "-"
+
+ row = (
+ time.strftime("%Y-%m-%d", time.localtime(tx["time"])),
+ label,
+ tx["category"],
+ round(tx["amount"], (0 if specter.unit == "sat" else 8)),
+ round(amount_price * 100) / 100 if amount_price is not None else "-",
+ rate,
+ tx["txid"],
+ tx["address"],
+ tx["blockheight"],
+ tx["time"],
+ )
+ if not wallet:
+ row = (tx.get("wallet_alias", ""),) + row
+ w.writerow(row)
+ yield data.getvalue()
+ data.seek(0)
+ data.truncate(0)
+
+
+# Addresses list to user-friendly CSV format
+def addresses_list_to_csv(wallet):
+ data = StringIO()
+ w = csv.writer(data)
+ # write header
+ row = (
+ _("Address"),
+ _("Label"),
+ _("Index"),
+ _("Used"),
+ _("Current balance"),
+ )
+ w.writerow(row)
+ yield data.getvalue()
+ data.seek(0)
+ data.truncate(0)
+
+ # write each log item
+ for address in wallet._addresses:
+ address_info = wallet.get_address_info(address)
+ if not address_info.is_labeled and not address_info.used:
+ continue
+ row = (
+ address,
+ address_info.label,
+ _("(external)")
+ if address_info.is_external
+ else (
+ str(address_info.index)
+ + (_(" (change)") if address_info.change else "")
+ ),
+ address_info.used,
+ )
+ if address_info.is_external:
+ balance_on_address = _("unknown (external address)")
+ else:
+ balance_on_address = 0
+ if address_info.used:
+ for tx in wallet.full_utxo:
+ if tx.get("address", "") == address:
+ balance_on_address += tx.get("amount", 0)
+ row += (balance_on_address,)
+
+ w.writerow(row)
+ yield data.getvalue()
+ data.seek(0)
+ data.truncate(0)
+
+
+def wallet_addresses_list_to_csv(addresses_list):
+ """Convert a list of the wallet addresses to user-friendly CSV format
+ Parameters: addresses_list: a dict of addresses informations
+ (index, address, type, label, used, utxo and amount)"""
+ data = StringIO()
+ w = csv.writer(data)
+ # write header
+ row = (
+ _("Index"),
+ _("Address"),
+ _("Type"),
+ _("Label"),
+ _("Used"),
+ _("UTXO"),
+ _("Amount (BTC)"),
+ )
+ w.writerow(row)
+ yield data.getvalue()
+ data.seek(0)
+ data.truncate(0)
+
+ # write each log item
+ for address_item in addresses_list:
+
+ used = "Yes" if address_item["used"] else "No"
+
+ row = (
+ address_item["index"],
+ address_item["address"],
+ address_item["type"],
+ address_item["label"],
+ used,
+ address_item["utxo"],
+ address_item["amount"],
+ )
+
+ w.writerow(row)
+ yield data.getvalue()
+ data.seek(0)
+ data.truncate(0)
+
+
+def process_txlist(txlist, idx=0, limit=100, search=None, sortby=None, sortdir="asc"):
+ if search:
+ txlist = [
+ tx
+ for tx in txlist
+ if search in tx["txid"]
+ or (
+ any(search in address for address in tx["address"])
+ if isinstance(tx["address"], list)
+ else search in tx["address"]
+ )
+ or (
+ any(search in label for label in tx.get("label", ""))
+ if isinstance(tx.get("label", ""), list)
+ else search in tx.get("label", "")
+ )
+ or (
+ any(search in str(amount) for amount in tx["amount"])
+ if isinstance(tx["amount"], list)
+ else search in str(tx["amount"])
+ )
+ or search in str(tx["confirmations"])
+ or search in str(tx["time"])
+ or search
+ in str(format(datetime.fromtimestamp(tx["time"]), "%d.%m.%Y %H:%M"))
+ ]
+ if sortby:
+
+ def sort(tx):
+ val = tx.get(sortby, None)
+ final = val
+ if val:
+ if isinstance(val, list):
+ if isinstance(val[0], Number):
+ final = sum(val)
+ elif isinstance(val[0], str):
+ final = sorted(
+ val, key=lambda s: s.lower(), reverse=sortdir != "asc"
+ )[0].lower()
+ elif isinstance(val, str):
+ final = val.lower()
+ return final
+
+ txlist = sorted(txlist, key=sort, reverse=sortdir != "asc")
+ if limit:
+ page_count = (len(txlist) // limit) + (0 if len(txlist) % limit == 0 else 1)
+ txlist = txlist[limit * idx : limit * (idx + 1)]
+ else:
+ page_count = 1
+ # add assets
+ if app.specter.is_liquid:
+ for tx in txlist:
+ if "asset" in tx:
+ if isinstance(tx["asset"], list):
+ tx["assetlabel"] = [
+ app.specter.asset_label(asset) for asset in tx["asset"]
+ ]
+ else:
+ tx["assetlabel"] = app.specter.asset_label(tx["asset"])
+ return {"txlist": json.dumps(txlist), "pageCount": page_count}
+
+
+def process_addresses_list(
+ addresses_list, idx=0, limit=100, sortby=None, sortdir="asc"
+):
+ """Receive an address list as parameter and sort it or slice it for pagination.
+ Parameters: addresses_list: list of dict with the keys
+ (index, address, label, used, utxo, amount)
+ idx: pagination index (current page)
+ limit: maximum number of items on the page
+ sortby: field by which the list will be ordered
+ (index, address, label, used, utxo, amount)
+ sortdir: 'asc' (ascending) or 'desc' (descending) order"""
+ if sortby:
+
+ def sort(addr):
+ val = addr.get(sortby, None)
+ final = val
+ if val:
+ if isinstance(val, str):
+ final = val.lower()
+ return final
+
+ addresses_list = sorted(addresses_list, key=sort, reverse=sortdir != "asc")
+
+ if limit:
+ page_count = (len(addresses_list) // limit) + (
+ 0 if len(addresses_list) % limit == 0 else 1
+ )
+ addresses_list = addresses_list[limit * idx : limit * (idx + 1)]
+ else:
+ page_count = 1
+
+ return {"addressesList": addresses_list, "pageCount": page_count}
diff --git a/src/cryptoadvance/specter/templates/includes/address-data.html b/src/cryptoadvance/specter/templates/includes/address-data.html
index fc4e8e24f..5b95a087c 100644
--- a/src/cryptoadvance/specter/templates/includes/address-data.html
+++ b/src/cryptoadvance/specter/templates/includes/address-data.html
@@ -49,7 +49,7 @@
}
async fetchAddressData() {
- let url = `{{ url_for('wallets_endpoint.addressinfo', wallet_alias='WALLET_ALIAS') }}`.replace("WALLET_ALIAS", this.wallet);
+ let url = `{{ url_for('wallets_endpoint_api.addressinfo', wallet_alias='WALLET_ALIAS') }}`.replace("WALLET_ALIAS", this.wallet);
var formData = new FormData();
formData.append('address', this.address);
formData.append('csrf_token', '{{ csrf_token() }}');
diff --git a/src/cryptoadvance/specter/templates/includes/address-label.html b/src/cryptoadvance/specter/templates/includes/address-label.html
index a0aa09639..2c41e72c1 100644
--- a/src/cryptoadvance/specter/templates/includes/address-label.html
+++ b/src/cryptoadvance/specter/templates/includes/address-label.html
@@ -206,7 +206,7 @@
}
async fetchAddressLabel() {
- let url = `{{ url_for('wallets_endpoint.get_label', wallet_alias='WALLET_ALIAS') }}`.replace("WALLET_ALIAS", this.wallet);
+ let url = `{{ url_for('wallets_endpoint_api.get_label', wallet_alias='WALLET_ALIAS') }}`.replace("WALLET_ALIAS", this.wallet);
var formData = new FormData();
formData.append('address', this.address);
formData.append('csrf_token', '{{ csrf_token() }}');
@@ -237,7 +237,7 @@
}
async setAddressLabel() {
- let url = `{{ url_for('wallets_endpoint.set_label', wallet_alias='WALLET_ALIAS') }}`.replace("WALLET_ALIAS", this.wallet);
+ let url = `{{ url_for('wallets_endpoint_api.set_label', wallet_alias='WALLET_ALIAS') }}`.replace("WALLET_ALIAS", this.wallet);
var formData = new FormData();
formData.append('address', this.address);
formData.append('label', this.label.innerText);
diff --git a/src/cryptoadvance/specter/templates/includes/address-row.html b/src/cryptoadvance/specter/templates/includes/address-row.html
index 93ef213e9..6fb2e9d34 100644
--- a/src/cryptoadvance/specter/templates/includes/address-row.html
+++ b/src/cryptoadvance/specter/templates/includes/address-row.html
@@ -12,7 +12,7 @@
+
+
+
\ No newline at end of file
diff --git a/src/cryptoadvance/specter/templates/includes/merkletooltip.html b/src/cryptoadvance/specter/templates/includes/merkletooltip.html
index f172db339..24b5989bf 100644
--- a/src/cryptoadvance/specter/templates/includes/merkletooltip.html
+++ b/src/cryptoadvance/specter/templates/includes/merkletooltip.html
@@ -1,4 +1,4 @@
-
+
{{ _('Specter-Desktop validates BIP 37 Merkle Proofs from your full node guaranteeing that transactions displayed as confirmed have been included in the specified block hash.') }}
{{ _("However, a compromised full node could return a fake block that was never mined into Bitcoin's blockchain!") }}
{{ _("If you find the displayed block hash in other locations (other nodes, block explorers, etc) and you trust specter-desktop isn't lying to you, then you can be sure this transaction exists in their blockchain as well.") }}"
diff --git a/src/cryptoadvance/specter/templates/includes/sidebar/components/sidebar_wallet_list_item.jinja b/src/cryptoadvance/specter/templates/includes/sidebar/components/sidebar_wallet_list_item.jinja
index bc832edcb..68df8fa94 100644
--- a/src/cryptoadvance/specter/templates/includes/sidebar/components/sidebar_wallet_list_item.jinja
+++ b/src/cryptoadvance/specter/templates/includes/sidebar/components/sidebar_wallet_list_item.jinja
@@ -37,7 +37,7 @@
let walletRescanActive = true;
async function fetchWalletRescanProgress() {
try {
- let url="{{ url_for('wallets_endpoint.rescan_progress', wallet_alias=wallet.alias) }}"
+ let url="{{ url_for('wallets_endpoint_api.rescan_progress', wallet_alias=wallet.alias) }}"
const response = await fetch(
url,
{
diff --git a/src/cryptoadvance/specter/templates/includes/tx-data.html b/src/cryptoadvance/specter/templates/includes/tx-data.html
index 0fa589219..f40562021 100644
--- a/src/cryptoadvance/specter/templates/includes/tx-data.html
+++ b/src/cryptoadvance/specter/templates/includes/tx-data.html
@@ -44,7 +44,7 @@
}
async fetchRawTx(txid) {
- let url = `{{ url_for('wallets_endpoint.decoderawtx', wallet_alias='WALLET_ALIAS') }}`.replace("WALLET_ALIAS", this.wallet);
+ let url = `{{ url_for('wallets_endpoint_api.decoderawtx', wallet_alias='WALLET_ALIAS') }}`.replace("WALLET_ALIAS", this.wallet);
var formData = new FormData();
formData.append('txid', txid);
formData.append('csrf_token', '{{ csrf_token() }}');
@@ -257,7 +257,7 @@
}
async fetchAddressIsMine(address) {
- let url = `{{ url_for('wallets_endpoint.addressinfo', wallet_alias='WALLET_ALIAS') }}`.replace("WALLET_ALIAS", this.wallet);
+ let url = `{{ url_for('wallets_endpoint_api.addressinfo', wallet_alias='WALLET_ALIAS') }}`.replace("WALLET_ALIAS", this.wallet);
var formData = new FormData();
formData.append('address', address)
formData.append('csrf_token', '{{ csrf_token() }}');
diff --git a/src/cryptoadvance/specter/templates/includes/tx-row.html b/src/cryptoadvance/specter/templates/includes/tx-row.html
index b3bda765a..fce499fc2 100644
--- a/src/cryptoadvance/specter/templates/includes/tx-row.html
+++ b/src/cryptoadvance/specter/templates/includes/tx-row.html
@@ -18,23 +18,29 @@