mirror of
https://github.com/cryptoadvance/specter-desktop.git
synced 2026-08-13 12:33:29 +02:00
* Prompting rescan hint within tx-table when no transactions appear * Animation of rescan part after being forwarded * Basics option added in test-cypress for faster development on cypress tests on slower machines * addWallet command added in Cypress * Page limit in addresses view fixed Co-authored-by: moneymanolis <moneymanolis@protonmail.com>
841 lines
32 KiB
Python
841 lines
32 KiB
Python
import json
|
|
import logging
|
|
import random
|
|
from functools import wraps
|
|
|
|
import requests
|
|
from cryptoadvance.specter.commands.psbt_creator import PsbtCreator
|
|
from cryptoadvance.specter.util.wallet_importer import WalletImporter
|
|
from cryptoadvance.specter.wallet import Wallet
|
|
from cryptoadvance.specter.util.tx import is_hex, convert_rawtransaction_to_psbt
|
|
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 login_required
|
|
|
|
from ..helpers import bcur2base64, get_devices_with_keys_by_type, get_txid
|
|
from ..key import Key
|
|
from ..managers.wallet_manager import purposes
|
|
from ..persistence import delete_file
|
|
from ..specter_error import SpecterError, handle_exception
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
rand = random.randint(0, 1e32) # to force style refresh
|
|
|
|
# Setup endpoint blueprint
|
|
wallets_endpoint = Blueprint("wallets_endpoint", __name__)
|
|
|
|
|
|
def handle_wallet_error(func_name, error):
|
|
flash(_("SpecterError while {}: {}").format(func_name, error), "error")
|
|
app.logger.error(f"SpecterError while {func_name}: {error}")
|
|
app.specter.wallet_manager.update()
|
|
return redirect(url_for("welcome_endpoint.about"))
|
|
|
|
|
|
def check_wallet(func):
|
|
@wraps(func)
|
|
def wrapper(*args, **kwargs):
|
|
"""checks the wallet for healthiness A wrapper function"""
|
|
if kwargs["wallet_alias"]:
|
|
wallet_alias = kwargs["wallet_alias"]
|
|
wallet = app.specter.wallet_manager.get_by_alias(wallet_alias)
|
|
wallet.get_info()
|
|
return func(*args, **kwargs)
|
|
|
|
return wrapper
|
|
|
|
|
|
################## Wallet overview #######################
|
|
|
|
|
|
@wallets_endpoint.route("/wallets_overview/")
|
|
@login_required
|
|
def wallets_overview():
|
|
app.specter.check_blockheight()
|
|
for wallet in list(app.specter.wallet_manager.wallets.values()):
|
|
wallet.update_balance()
|
|
wallet.check_utxo()
|
|
|
|
return render_template(
|
|
"wallet/wallets_overview.jinja",
|
|
specter=app.specter,
|
|
rand=rand,
|
|
services=app.specter.service_manager.services,
|
|
)
|
|
|
|
|
|
################## Failed wallets fix #######################
|
|
|
|
|
|
@wallets_endpoint.route("/failed_wallets/", methods=["POST"])
|
|
@login_required
|
|
def failed_wallets():
|
|
if request.method == "POST":
|
|
action = request.form["action"]
|
|
if action == "retry_loading_wallets":
|
|
app.specter.wallet_manager.update()
|
|
elif action == "delete_failed_wallet":
|
|
try:
|
|
wallet = json.loads(request.form["wallet_data"])
|
|
fullpath = wallet["fullpath"]
|
|
delete_file(fullpath)
|
|
delete_file(fullpath + ".bkp")
|
|
delete_file(fullpath.replace(".json", "_addr.csv"))
|
|
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("/")
|
|
|
|
|
|
################## New wallet #######################
|
|
|
|
|
|
@wallets_endpoint.route("/new_wallet/")
|
|
@login_required
|
|
def new_wallet_type():
|
|
err = None
|
|
if app.specter.chain is None:
|
|
err = _("Configure Bitcoin Core to create wallets")
|
|
return render_template("base.jinja", error=err, specter=app.specter, rand=rand)
|
|
try:
|
|
# Make sure wallet is enabled on Bitcoin Core
|
|
app.specter.rpc.listwallets()
|
|
except Exception as e:
|
|
handle_exception(e)
|
|
# Hmm, would be better to be more precise with this exception. Best assumption:
|
|
err = _(
|
|
'<p><br>Bitcoin Core is running with wallets disabled.<br><br>Please make sure disablewallet is off (set disablewallet=0 in your bitcoin.conf), then restart Bitcoin Core and try again.<br>See <a href="https://github.com/cryptoadvance/specter-desktop/blob/34ca139694ecafb2e7c2bd5ad5c4ac74c6d11501/docs/faq.md#im-not-sure-i-want-the-bitcoin-core-wallet-functionality-to-be-used-is-that-mandatory-if-so-is-it-considered-secure" target="_blank" style="color: white;">here</a> for more information.</p>'
|
|
)
|
|
return render_template("base.jinja", error=err, specter=app.specter, rand=rand)
|
|
return render_template(
|
|
"wallet/new_wallet/new_wallet_type.jinja", specter=app.specter, rand=rand
|
|
)
|
|
|
|
|
|
@wallets_endpoint.route("/new_wallet/<wallet_type>/", methods=["GET", "POST"])
|
|
@login_required
|
|
def new_wallet(wallet_type):
|
|
wallet_types = ["simple", "multisig", "import_wallet"]
|
|
if wallet_type not in wallet_types:
|
|
flash(_("Unknown wallet type requested"), "error")
|
|
return redirect(url_for("wallets_endpoint.new_wallet_type"))
|
|
|
|
err = None
|
|
if request.method == "POST":
|
|
action = request.form["action"]
|
|
if action == "importwallet":
|
|
try:
|
|
wallet_importer: WalletImporter = WalletImporter(
|
|
request.form["wallet_data"],
|
|
app.specter,
|
|
)
|
|
except SpecterError as se:
|
|
flash(str(se), "error")
|
|
return redirect(url_for("wallets_endpoint.new_wallet_type"))
|
|
createwallet = "createwallet" in request.form
|
|
if createwallet:
|
|
# User might have renamed it
|
|
wallet_importer.wallet_name = request.form["wallet_name"]
|
|
wallet_importer.create_nonexisting_signers(
|
|
app.specter.device_manager, request.form
|
|
)
|
|
try:
|
|
wallet_importer.create_wallet(app.specter.wallet_manager)
|
|
except SpecterError as se:
|
|
flash(str(se), "error")
|
|
return redirect(url_for("wallets_endpoint.new_wallet_type"))
|
|
flash(_("Wallet imported successfully"), "info")
|
|
try:
|
|
wallet_importer.rescan_as_needed(app.specter)
|
|
except SpecterError as se:
|
|
flash(str(se), "error")
|
|
return redirect(
|
|
url_for(
|
|
"wallets_endpoint.receive",
|
|
wallet_alias=wallet_importer.wallet.alias,
|
|
)
|
|
+ "?newwallet=true"
|
|
)
|
|
else:
|
|
return render_template(
|
|
"wallet/new_wallet/import_wallet.jinja",
|
|
wallet_data=wallet_importer.wallet_json,
|
|
wallet_type=wallet_importer.wallet_type,
|
|
wallet_name=wallet_importer.wallet_name,
|
|
cosigners=wallet_importer.cosigners,
|
|
unknown_cosigners=wallet_importer.unknown_cosigners,
|
|
unknown_cosigners_types=wallet_importer.unknown_cosigners_types,
|
|
sigs_required=wallet_importer.sigs_required,
|
|
sigs_total=wallet_importer.sigs_total,
|
|
specter=app.specter,
|
|
rand=rand,
|
|
)
|
|
if action == "device":
|
|
cosigners = [
|
|
app.specter.device_manager.get_by_alias(alias)
|
|
for alias in request.form.getlist("devices")
|
|
]
|
|
|
|
if not cosigners:
|
|
return render_template(
|
|
"wallet/new_wallet/new_wallet.jinja",
|
|
wallet_type=wallet_type,
|
|
error=_(
|
|
"No device was selected. Please select a device to create the wallet for."
|
|
),
|
|
specter=app.specter,
|
|
rand=rand,
|
|
)
|
|
devices = get_devices_with_keys_by_type(app, cosigners, wallet_type)
|
|
for device in devices:
|
|
if len(device.keys) == 0:
|
|
err = _(
|
|
"Device {} doesn't have keys matching this wallet type"
|
|
).format(device.name)
|
|
break
|
|
|
|
name = wallet_type.title()
|
|
wallet_name = name
|
|
i = 2
|
|
while wallet_name in app.specter.wallet_manager.wallets_names:
|
|
wallet_name = "%s %d" % (name, i)
|
|
i += 1
|
|
|
|
return render_template(
|
|
"wallet/new_wallet/new_wallet_keys.jinja",
|
|
cosigners=devices,
|
|
wallet_type=wallet_type,
|
|
sigs_total=len(devices),
|
|
sigs_required=max(len(devices) * 2 // 3, 1),
|
|
error=err,
|
|
specter=app.specter,
|
|
rand=rand,
|
|
)
|
|
if action == "key" and err is None:
|
|
wallet_name = request.form["wallet_name"]
|
|
address_type = request.form["type"]
|
|
sigs_total = int(request.form.get("sigs_total", 1))
|
|
sigs_required = int(request.form.get("sigs_required", 1))
|
|
if wallet_name in app.specter.wallet_manager.wallets_names:
|
|
err = _("Wallet already exists")
|
|
if err:
|
|
devices = [
|
|
app.specter.device_manager.get_by_alias(
|
|
request.form.get("cosigner{}".format(i))
|
|
)
|
|
for i in range(0, sigs_total)
|
|
]
|
|
return render_template(
|
|
"wallet/new_wallet/new_wallet_keys.jinja",
|
|
cosigners=devices,
|
|
wallet_type=wallet_type,
|
|
sigs_total=len(devices),
|
|
sigs_required=max(len(devices) * 2 // 3, 1),
|
|
error=err,
|
|
specter=app.specter,
|
|
rand=rand,
|
|
)
|
|
|
|
keys = []
|
|
cosigners = []
|
|
devices = []
|
|
for i in range(sigs_total):
|
|
try:
|
|
key = request.form["key%d" % i]
|
|
cosigner_name = request.form["cosigner%d" % i]
|
|
cosigner = app.specter.device_manager.get_by_alias(cosigner_name)
|
|
cosigners.append(cosigner)
|
|
for k in cosigner.keys:
|
|
if k.original == key:
|
|
keys.append(k)
|
|
break
|
|
except:
|
|
pass
|
|
if len(keys) != sigs_total or len(cosigners) != sigs_total:
|
|
err = _(
|
|
"No keys were selected for device, please try adding keys first"
|
|
)
|
|
devices = [
|
|
app.specter.device_manager.get_by_alias(
|
|
request.form.get("cosigner{}".format(i))
|
|
)
|
|
for i in range(0, sigs_total)
|
|
]
|
|
return render_template(
|
|
"wallet/new_wallet/new_wallet_keys.jinja",
|
|
cosigners=devices,
|
|
wallet_type=wallet_type,
|
|
sigs_total=len(devices),
|
|
sigs_required=max(len(devices) * 2 // 3, 1),
|
|
error=err,
|
|
specter=app.specter,
|
|
rand=rand,
|
|
)
|
|
|
|
# create a wallet here
|
|
try:
|
|
wallet = app.specter.wallet_manager.create_wallet(
|
|
wallet_name, sigs_required, address_type, keys, cosigners
|
|
)
|
|
except Exception as e:
|
|
handle_exception(e)
|
|
err = _("Failed to create wallet. Error: {}").format(e)
|
|
return render_template(
|
|
"wallet/new_wallet/new_wallet_keys.jinja",
|
|
cosigners=cosigners,
|
|
wallet_type=wallet_type,
|
|
sigs_total=len(devices),
|
|
sigs_required=max(len(devices) * 2 // 3, 1),
|
|
error=err,
|
|
specter=app.specter,
|
|
rand=rand,
|
|
)
|
|
|
|
app.logger.info("Created Wallet %s" % wallet_name)
|
|
rescan_blockchain = "rescanblockchain" in request.form
|
|
if rescan_blockchain:
|
|
# old wallet - import more addresses
|
|
wallet.keypoolrefill(0, wallet.IMPORT_KEYPOOL, change=False)
|
|
wallet.keypoolrefill(0, wallet.IMPORT_KEYPOOL, change=True)
|
|
if "utxo" in request.form.get("full_rescan_option"):
|
|
explorer = None
|
|
if "use_explorer" in request.form:
|
|
if request.form["explorer"] == "CUSTOM":
|
|
explorer = request.form["custom_explorer"]
|
|
else:
|
|
explorer = app.config["EXPLORERS_LIST"][
|
|
request.form["explorer"]
|
|
]["url"]
|
|
wallet.rescanutxo(
|
|
explorer,
|
|
app.specter.requests_session(explorer.endswith(".onion")),
|
|
app.specter.only_tor,
|
|
)
|
|
app.specter.info["utxorescan"] = 1
|
|
app.specter.utxorescanwallet = wallet.alias
|
|
else:
|
|
app.logger.info("Rescanning Blockchain ...")
|
|
startblock = int(request.form["startblock"])
|
|
try:
|
|
wallet.rpc.rescanblockchain(startblock, no_wait=True)
|
|
except Exception as e:
|
|
handle_exception(e)
|
|
err = "%r" % e
|
|
wallet.getdata()
|
|
return redirect(
|
|
url_for("wallets_endpoint.receive", wallet_alias=wallet.alias)
|
|
+ "?newwallet=true"
|
|
)
|
|
if action == "preselected_device":
|
|
return render_template(
|
|
"wallet/new_wallet/new_wallet_keys.jinja",
|
|
cosigners=[
|
|
app.specter.device_manager.get_by_alias(request.form["device"])
|
|
],
|
|
wallet_type="simple",
|
|
sigs_total=1,
|
|
sigs_required=1,
|
|
error=err,
|
|
specter=app.specter,
|
|
rand=rand,
|
|
)
|
|
|
|
return render_template(
|
|
"wallet/new_wallet/new_wallet.jinja",
|
|
wallet_type=wallet_type,
|
|
error=err,
|
|
specter=app.specter,
|
|
rand=rand,
|
|
)
|
|
|
|
|
|
################## Wallet pages #######################
|
|
|
|
###### Wallet index page ######
|
|
@wallets_endpoint.route("/wallet/<wallet_alias>/")
|
|
@login_required
|
|
def wallet(wallet_alias):
|
|
wallet = app.specter.wallet_manager.get_by_alias(wallet_alias)
|
|
if wallet.amount_total > 0:
|
|
return redirect(url_for("wallets_endpoint.history", wallet_alias=wallet_alias))
|
|
else:
|
|
return redirect(url_for("wallets_endpoint.receive", wallet_alias=wallet_alias))
|
|
|
|
|
|
###### Wallet transaction history ######
|
|
@wallets_endpoint.route("/wallet/<wallet_alias>/history/", methods=["GET", "POST"])
|
|
@login_required
|
|
def history(wallet_alias):
|
|
wallet = app.specter.wallet_manager.get_by_alias(wallet_alias)
|
|
tx_list_type = "txlist"
|
|
|
|
if request.method == "POST":
|
|
action = request.form["action"]
|
|
if action == "freezeutxo":
|
|
wallet.toggle_freeze_utxo(request.form.getlist("selected_utxo"))
|
|
tx_list_type = "utxo"
|
|
elif action == "abandon_tx":
|
|
txid = request.form["txid"]
|
|
try:
|
|
wallet.abandontransaction(txid)
|
|
except SpecterError as e:
|
|
flash(str(e), "error")
|
|
|
|
# update balances in the wallet
|
|
app.specter.check_blockheight()
|
|
wallet.update_balance()
|
|
wallet.check_utxo()
|
|
|
|
return render_template(
|
|
"wallet/history/wallet_history.jinja",
|
|
wallet_alias=wallet_alias,
|
|
wallet=wallet,
|
|
tx_list_type=tx_list_type,
|
|
specter=app.specter,
|
|
rand=rand,
|
|
services=app.specter.service_manager.services,
|
|
)
|
|
|
|
|
|
###### Wallet receive ######
|
|
|
|
|
|
@wallets_endpoint.route("/wallet/<wallet_alias>/receive/", methods=["GET", "POST"])
|
|
@login_required
|
|
@check_wallet
|
|
def receive(wallet_alias):
|
|
wallet: Wallet = app.specter.wallet_manager.get_by_alias(wallet_alias)
|
|
if request.method == "POST":
|
|
action = request.form["action"]
|
|
if action == "newaddress":
|
|
wallet.getnewaddress()
|
|
elif action == "updatelabel":
|
|
label = request.form["label"]
|
|
wallet.setlabel(wallet.address, label)
|
|
# check that current address is unused
|
|
# and generate new one if it is
|
|
wallet.check_unused()
|
|
return render_template(
|
|
"wallet/receive/wallet_receive.jinja",
|
|
wallet_alias=wallet_alias,
|
|
wallet=wallet,
|
|
specter=app.specter,
|
|
rand=rand,
|
|
)
|
|
|
|
|
|
###### Wallet send ######
|
|
|
|
|
|
@wallets_endpoint.route("/wallet/<wallet_alias>/send")
|
|
@login_required
|
|
def send(wallet_alias):
|
|
wallet = app.specter.wallet_manager.get_by_alias(wallet_alias)
|
|
if len(wallet.pending_psbts) > 0:
|
|
return redirect(
|
|
url_for("wallets_endpoint.send_pending", wallet_alias=wallet_alias)
|
|
)
|
|
else:
|
|
return redirect(url_for("wallets_endpoint.send_new", wallet_alias=wallet_alias))
|
|
|
|
|
|
@wallets_endpoint.route("/wallet/<wallet_alias>/send/new", methods=["GET", "POST"])
|
|
@login_required
|
|
def send_new(wallet_alias):
|
|
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()
|
|
psbt = None
|
|
addresses = [""]
|
|
labels = [""]
|
|
amounts = [0]
|
|
amount_units = ["btc"]
|
|
err = None
|
|
ui_option = "ui"
|
|
recipients_txt = ""
|
|
fillform = False
|
|
subtract = False
|
|
subtract_from = 1
|
|
fee_options = "dynamic"
|
|
rbf = not app.specter.is_liquid
|
|
rbf_utxo = []
|
|
rbf_tx_id = ""
|
|
selected_coins = request.form.getlist("coinselect")
|
|
# Additional server side check not to use frozen UTXO as a precaution
|
|
frozen_utxo = wallet.frozen_utxo
|
|
for utxo in selected_coins:
|
|
if utxo in frozen_utxo:
|
|
selected_coins.remove(utxo)
|
|
flash(f"You've selected a frozen UTXO for a transaction.", "error")
|
|
return redirect(
|
|
url_for("wallets_endpoint.history", wallet_alias=wallet_alias)
|
|
)
|
|
|
|
if request.method == "POST":
|
|
action = request.form.get("action")
|
|
rbf_tx_id = request.form.get("rbf_tx_id", "")
|
|
if action == "createpsbt":
|
|
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"),
|
|
)
|
|
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:
|
|
rbf_tx_id = request.form["rbf_tx_id"]
|
|
rbf_fee_rate = float(request.form["rbf_fee_rate"])
|
|
|
|
if action == "rbf":
|
|
psbt = wallet.bumpfee(rbf_tx_id, rbf_fee_rate)
|
|
elif action == "rbf_cancel":
|
|
psbt = wallet.canceltx(rbf_tx_id, rbf_fee_rate)
|
|
else:
|
|
raise SpecterError("Invalid action")
|
|
|
|
if psbt["fee_rate"] - rbf_fee_rate > wallet.MIN_FEE_RATE / 10:
|
|
flash(
|
|
_(
|
|
"We had to increase the fee rate from {} to {} sat/vbyte"
|
|
).format(rbf_fee_rate, psbt["fee_rate"])
|
|
)
|
|
return render_template(
|
|
"wallet/send/sign/wallet_send_sign_psbt.jinja",
|
|
psbt=psbt,
|
|
labels=[],
|
|
wallet_alias=wallet_alias,
|
|
wallet=wallet,
|
|
specter=app.specter,
|
|
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)
|
|
)
|
|
elif action == "rbf_edit":
|
|
try:
|
|
decoded_tx = wallet.decode_tx(rbf_tx_id)
|
|
addresses = decoded_tx["addresses"]
|
|
amounts = decoded_tx["amounts"]
|
|
amount_units = decoded_tx.get("assets", ["btc"] * len(addresses))
|
|
# get_label returns a label or address if no label is set
|
|
labels = [wallet.getlabel(addr) for addr in addresses]
|
|
# set empty label to addresses that don't have labels
|
|
labels = [
|
|
label if label != addr else ""
|
|
for addr, label in zip(addresses, labels)
|
|
]
|
|
|
|
selected_coins = [
|
|
f"{utxo['txid']}, {utxo['vout']}"
|
|
for utxo in decoded_tx["used_utxo"]
|
|
]
|
|
fee_rate = float(request.form["rbf_fee_rate"])
|
|
fee_options = "manual"
|
|
rbf = True
|
|
fillform = True
|
|
except Exception as e:
|
|
handle_exception(e)
|
|
flash(_("Failed to perform RBF. Error: {}").format(e), "error")
|
|
elif action == "signhotwallet":
|
|
passphrase = request.form["passphrase"]
|
|
psbt = json.loads(request.form["psbt"])
|
|
current_psbt = wallet.pending_psbts[psbt["tx"]["txid"]]
|
|
b64psbt = str(current_psbt)
|
|
device = request.form["device"]
|
|
if "devices_signed" not in psbt or device not in psbt["devices_signed"]:
|
|
try:
|
|
# get device and sign with it
|
|
signed_psbt = app.specter.device_manager.get_by_alias(
|
|
device
|
|
).sign_psbt(b64psbt, wallet, passphrase)
|
|
raw = None
|
|
if signed_psbt["complete"]:
|
|
raw = wallet.rpc.finalizepsbt(b64psbt)
|
|
current_psbt.update(signed_psbt["psbt"], raw)
|
|
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:
|
|
signed_psbt = None
|
|
flash(_("Device already signed the PSBT"), "error")
|
|
return render_template(
|
|
"wallet/send/sign/wallet_send_sign_psbt.jinja",
|
|
signed_psbt=signed_psbt,
|
|
psbt=psbt,
|
|
labels=labels,
|
|
wallet_alias=wallet_alias,
|
|
wallet=wallet,
|
|
specter=app.specter,
|
|
rand=rand,
|
|
)
|
|
elif action == "fillform":
|
|
# TODO: Not yet used. Remove if the use case doesn't happen.
|
|
# can be used to recommend a transaction from a service (goind to an exchange or so)
|
|
addresses = request.form.getlist("addresses[]")
|
|
labels = request.form.getlist("labels[]")
|
|
amounts = request.form.getlist("amounts[]")
|
|
fillform = True
|
|
|
|
if rbf_tx_id:
|
|
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 not rbf
|
|
)
|
|
wallet_utxo = wallet.utxo
|
|
if app.specter.is_liquid:
|
|
for tx in wallet_utxo + rbf_utxo:
|
|
if "asset" in tx:
|
|
tx["assetlabel"] = app.specter.asset_label(tx.get("asset"))
|
|
return render_template(
|
|
"wallet/send/new/wallet_send.jinja",
|
|
psbt=psbt,
|
|
ui_option=ui_option,
|
|
recipients_txt=recipients_txt,
|
|
addresses=addresses,
|
|
labels=labels,
|
|
amounts=amounts,
|
|
fillform=fillform,
|
|
recipients=list(zip(addresses, amounts, amount_units, labels)),
|
|
subtract=subtract,
|
|
subtract_from=subtract_from,
|
|
fee_options=fee_options,
|
|
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,
|
|
wallet_alias=wallet_alias,
|
|
wallet=wallet,
|
|
specter=app.specter,
|
|
rand=rand,
|
|
error=err,
|
|
)
|
|
|
|
|
|
@wallets_endpoint.route("/wallet/<wallet_alias>/send/pending/", methods=["GET", "POST"])
|
|
@login_required
|
|
def send_pending(wallet_alias):
|
|
wallet = app.specter.wallet_manager.get_by_alias(wallet_alias)
|
|
if request.method == "POST":
|
|
action = request.form["action"]
|
|
if action == "deletepsbt":
|
|
try:
|
|
wallet.delete_pending_psbt(
|
|
json.loads(request.form["pending_psbt"])["tx"]["txid"]
|
|
)
|
|
except Exception as e:
|
|
handle_exception(e)
|
|
flash(_("Could not delete Pending PSBT!"), "error")
|
|
elif action == "openpsbt":
|
|
psbt = json.loads(request.form["pending_psbt"])
|
|
return render_template(
|
|
"wallet/send/sign/wallet_send_sign_psbt.jinja",
|
|
psbt=psbt,
|
|
wallet_alias=wallet_alias,
|
|
wallet=wallet,
|
|
specter=app.specter,
|
|
rand=rand,
|
|
)
|
|
pending_psbts = wallet.pending_psbts_dict()
|
|
return render_template(
|
|
"wallet/send/pending/wallet_sendpending.jinja",
|
|
pending_psbts=pending_psbts,
|
|
wallet_alias=wallet_alias,
|
|
wallet=wallet,
|
|
specter=app.specter,
|
|
)
|
|
|
|
|
|
@wallets_endpoint.route("/wallet/<wallet_alias>/send/import", methods=["GET", "POST"])
|
|
@login_required
|
|
def import_psbt(wallet_alias):
|
|
wallet = app.specter.wallet_manager.get_by_alias(wallet_alias)
|
|
if request.method == "POST":
|
|
action = request.form["action"]
|
|
if action == "importpsbt":
|
|
try:
|
|
b64psbt = "".join(request.form["rawpsbt"].split())
|
|
psbt = wallet.importpsbt(
|
|
convert_rawtransaction_to_psbt(wallet.rpc, b64psbt)
|
|
if is_hex(b64psbt)
|
|
else 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)
|
|
)
|
|
return render_template(
|
|
"wallet/send/sign/wallet_send_sign_psbt.jinja",
|
|
psbt=psbt,
|
|
wallet_alias=wallet_alias,
|
|
wallet=wallet,
|
|
specter=app.specter,
|
|
rand=rand,
|
|
)
|
|
err = None
|
|
return render_template(
|
|
"wallet/send/import/wallet_importpsbt.jinja",
|
|
wallet_alias=wallet_alias,
|
|
wallet=wallet,
|
|
specter=app.specter,
|
|
rand=rand,
|
|
error=err,
|
|
)
|
|
|
|
|
|
###### Wallet addresses ######
|
|
|
|
|
|
@wallets_endpoint.route("/wallet/<wallet_alias>/addresses/", methods=["GET"])
|
|
@login_required
|
|
def addresses(wallet_alias):
|
|
"""Show informations about cached addresses (wallet._addresses) of the <wallet_alias>.
|
|
It updates balances in the wallet before renderization in order to show updated UTXO and
|
|
balance of each address."""
|
|
wallet = app.specter.wallet_manager.get_by_alias(wallet_alias)
|
|
|
|
# update balances in the wallet
|
|
app.specter.check_blockheight()
|
|
wallet.update_balance()
|
|
wallet.check_utxo()
|
|
|
|
return render_template(
|
|
"wallet/addresses/wallet_addresses.jinja",
|
|
wallet_alias=wallet_alias,
|
|
wallet=wallet,
|
|
specter=app.specter,
|
|
rand=rand,
|
|
services=app.specter.service_manager.services,
|
|
)
|
|
|
|
|
|
###### Wallet settings ######
|
|
|
|
|
|
@wallets_endpoint.route("/wallet/<wallet_alias>/settings/", methods=["GET", "POST"])
|
|
@login_required
|
|
def settings(wallet_alias):
|
|
wallet = app.specter.wallet_manager.get_by_alias(wallet_alias)
|
|
error = None
|
|
if request.method == "POST":
|
|
action = request.form["action"]
|
|
if action == "rescanblockchain":
|
|
startblock = int(request.form["startblock"])
|
|
try:
|
|
delete_file(wallet._transactions.path)
|
|
wallet.fetch_transactions()
|
|
|
|
# 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:
|
|
handle_exception(e)
|
|
error = "%r" % e
|
|
wallet.getdata()
|
|
elif action == "abortrescan":
|
|
res = wallet.rpc.abortrescan()
|
|
if not res:
|
|
error = _("Failed to abort rescan. Maybe already complete?")
|
|
wallet.getdata()
|
|
elif action == "rescanutxo":
|
|
explorer = None
|
|
if "use_explorer" in request.form:
|
|
if request.form["explorer"] == "CUSTOM":
|
|
explorer = request.form["custom_explorer"]
|
|
else:
|
|
explorer = app.config["EXPLORERS_LIST"][request.form["explorer"]][
|
|
"url"
|
|
]
|
|
|
|
wallet.rescanutxo(
|
|
explorer,
|
|
app.specter.requests_session(explorer and explorer.endswith(".onion")),
|
|
app.specter.only_tor,
|
|
)
|
|
app.specter.info["utxorescan"] = 1
|
|
app.specter.utxorescanwallet = wallet.alias
|
|
flash(
|
|
"Rescan started. Check the status bar on the left for progress and/or the logs for potential issues."
|
|
)
|
|
elif action == "abortrescanutxo":
|
|
app.specter.node.abortrescanutxo()
|
|
app.specter.info["utxorescan"] = None
|
|
app.specter.utxorescanwallet = None
|
|
flash(_("Successfully aborted the UTXO rescan"))
|
|
elif action == "import_address_labels":
|
|
address_labels = request.form["address_labels_data"]
|
|
imported_addresses_len = wallet.import_address_labels(address_labels)
|
|
if imported_addresses_len > 1:
|
|
flash(f"Successfully imported {imported_addresses_len} address labels.")
|
|
elif imported_addresses_len == 1:
|
|
flash(f"Successfully imported {imported_addresses_len} address label.")
|
|
else:
|
|
flash("No address labels were imported.")
|
|
elif action == "keypoolrefill":
|
|
delta = int(request.form["keypooladd"])
|
|
wallet.keypoolrefill(wallet.keypool, wallet.keypool + delta)
|
|
wallet.keypoolrefill(
|
|
wallet.change_keypool, wallet.change_keypool + delta, change=True
|
|
)
|
|
wallet.getdata()
|
|
elif action == "deletewallet":
|
|
app.specter.wallet_manager.delete_wallet(
|
|
wallet, app.specter.bitcoin_datadir, app.specter.chain
|
|
)
|
|
response = redirect(url_for("index"))
|
|
return response
|
|
elif action == "rename":
|
|
wallet_name = request.form["newtitle"]
|
|
if not wallet_name:
|
|
flash(_("Wallet name cannot be empty"), "error")
|
|
elif wallet_name == wallet.name:
|
|
pass
|
|
elif wallet_name in app.specter.wallet_manager.wallets_names:
|
|
flash(_("Wallet already exists"), "error")
|
|
else:
|
|
app.specter.wallet_manager.rename_wallet(wallet, wallet_name)
|
|
|
|
scroll_to_rescan_blockchain = request.args.get("rescan_blockchain")
|
|
return render_template(
|
|
"wallet/settings/wallet_settings.jinja",
|
|
purposes=purposes,
|
|
wallet_alias=wallet_alias,
|
|
wallet=wallet,
|
|
specter=app.specter,
|
|
rand=rand,
|
|
error=error,
|
|
scroll_to_rescan_blockchain=scroll_to_rescan_blockchain,
|
|
)
|