Litewallets

This commit is contained in:
gerlofvanek
2026-01-28 16:05:52 +01:00
parent a04ce28ca2
commit afae62ae38
37 changed files with 10525 additions and 272 deletions

View File

@@ -1253,7 +1253,8 @@ def amm_autostart_api(swap_client, post_string, params=None):
settings_path = os.path.join(swap_client.data_dir, cfg.CONFIG_FILENAME)
settings_path_new = settings_path + ".new"
shutil.copyfile(settings_path, settings_path + ".last")
if os.path.exists(settings_path):
shutil.copyfile(settings_path, settings_path + ".last")
with open(settings_path_new, "w") as fp:
json.dump(swap_client.settings, fp, indent=4)
shutil.move(settings_path_new, settings_path)

View File

@@ -218,6 +218,8 @@ def parseOfferFormData(swap_client, form_data, page_data, options={}):
if have_data_entry(form_data, "fee_from_extra"):
page_data["fee_from_extra"] = int(get_data_entry(form_data, "fee_from_extra"))
parsed_data["fee_from_extra"] = page_data["fee_from_extra"]
else:
page_data["fee_from_extra"] = 0
if have_data_entry(form_data, "fee_to_conf"):
page_data["fee_to_conf"] = int(get_data_entry(form_data, "fee_to_conf"))
@@ -226,6 +228,8 @@ def parseOfferFormData(swap_client, form_data, page_data, options={}):
if have_data_entry(form_data, "fee_to_extra"):
page_data["fee_to_extra"] = int(get_data_entry(form_data, "fee_to_extra"))
parsed_data["fee_to_extra"] = page_data["fee_to_extra"]
else:
page_data["fee_to_extra"] = 0
if have_data_entry(form_data, "check_offer"):
page_data["check_offer"] = True

View File

@@ -104,6 +104,10 @@ def page_settings(self, url_split, post_string):
"TODO: If running in docker see doc/tor.md to enable/disable tor."
)
electrum_supported_coins = (
"bitcoin",
"litecoin",
)
for name, c in swap_client.settings["chainclients"].items():
if have_data_entry(form_data, "apply_" + name):
data = {"lookups": get_data_entry(form_data, "lookups_" + name)}
@@ -138,10 +142,67 @@ def page_settings(self, url_split, post_string):
data["anon_tx_ring_size"] = int(
get_data_entry(form_data, "rct_ring_size_" + name)
)
if name in electrum_supported_coins:
new_connection_type = get_data_entry_or(
form_data, "connection_type_" + name, None
)
if new_connection_type and new_connection_type != c.get(
"connection_type"
):
coin_id = swap_client.getCoinIdFromName(name)
has_active_swaps = False
for bid_id, (bid, offer) in list(
swap_client.swaps_in_progress.items()
):
if (
offer.coin_from == coin_id
or offer.coin_to == coin_id
):
has_active_swaps = True
break
if has_active_swaps:
display_name = getCoinName(coin_id)
err_messages.append(
f"Cannot change {display_name} connection mode while swaps are in progress. "
f"Please wait for all {display_name} swaps to complete."
)
else:
data["connection_type"] = new_connection_type
if new_connection_type == "electrum":
data["manage_daemon"] = False
elif new_connection_type == "rpc":
data["manage_daemon"] = True
clearnet_servers = get_data_entry_or(
form_data, "electrum_clearnet_" + name, ""
).strip()
data["electrum_clearnet_servers"] = clearnet_servers
onion_servers = get_data_entry_or(
form_data, "electrum_onion_" + name, ""
).strip()
data["electrum_onion_servers"] = onion_servers
auto_transfer = have_data_entry(
form_data, "auto_transfer_" + name
)
data["auto_transfer_on_mode_switch"] = auto_transfer
# Address gap limit for scanning
gap_limit_str = get_data_entry_or(
form_data, "gap_limit_" + name, "20"
).strip()
try:
gap_limit = int(gap_limit_str)
if gap_limit < 5:
gap_limit = 5
elif gap_limit > 100:
gap_limit = 100
data["address_gap_limit"] = gap_limit
except ValueError:
pass
settings_changed, suggest_reboot = swap_client.editSettings(
name, data
settings_changed, suggest_reboot, migration_message = (
swap_client.editSettings(name, data)
)
if migration_message:
messages.append(migration_message)
if settings_changed is True:
messages.append("Settings applied.")
if suggest_reboot is True:
@@ -156,19 +217,65 @@ def page_settings(self, url_split, post_string):
display_name = getCoinName(swap_client.getCoinIdFromName(name))
messages.append(display_name + " disabled, shutting down.")
swap_client.stopRunning()
elif have_data_entry(form_data, "force_sweep_" + name):
coin_id = swap_client.getCoinIdFromName(name)
display_name = getCoinName(coin_id)
try:
result = swap_client.sweepLiteWalletFunds(coin_id)
if result.get("success"):
amount = result.get("amount", 0)
fee = result.get("fee", 0)
txid = result.get("txid", "")
messages.append(
f"Successfully swept {amount:.8f} {display_name} to RPC wallet. "
f"Fee: {fee:.8f}. TXID: {txid} (1 confirmation required)"
)
elif result.get("skipped"):
messages.append(
f"{display_name}: {result.get('reason', 'Sweep skipped')}"
)
else:
err_messages.append(
f"{display_name}: Sweep failed - {result.get('error', 'Unknown error')}"
)
except Exception as e:
err_messages.append(f"{display_name}: Sweep failed - {str(e)}")
except InactiveCoin as ex:
err_messages.append("InactiveCoin {}".format(Coins(ex.coinid).name))
except Exception as e:
err_messages.append(str(e))
chains_formatted = []
electrum_supported_coins = (
"bitcoin",
"litecoin",
)
sorted_names = sorted(swap_client.settings["chainclients"].keys())
from basicswap.interface.electrumx import (
DEFAULT_ELECTRUM_SERVERS,
DEFAULT_ONION_SERVERS,
)
for name in sorted_names:
c = swap_client.settings["chainclients"][name]
try:
display_name = getCoinName(swap_client.getCoinIdFromName(name))
except Exception:
display_name = name
clearnet_servers = c.get("electrum_clearnet_servers", None)
onion_servers = c.get("electrum_onion_servers", None)
if clearnet_servers is None:
default_clearnet = DEFAULT_ELECTRUM_SERVERS.get(name, [])
clearnet_servers = [f"{s['host']}:{s['port']}" for s in default_clearnet]
if onion_servers is None:
default_onion = DEFAULT_ONION_SERVERS.get(name, [])
onion_servers = [f"{s['host']}:{s['port']}" for s in default_onion]
clearnet_text = "\n".join(clearnet_servers) if clearnet_servers else ""
onion_text = "\n".join(onion_servers) if onion_servers else ""
chains_formatted.append(
{
"name": name,
@@ -176,6 +283,13 @@ def page_settings(self, url_split, post_string):
"lookups": c.get("chain_lookups", "local"),
"manage_daemon": c.get("manage_daemon", "Unknown"),
"connection_type": c.get("connection_type", "Unknown"),
"supports_electrum": name in electrum_supported_coins,
"clearnet_servers_text": clearnet_text,
"onion_servers_text": onion_text,
"auto_transfer_on_mode_switch": c.get(
"auto_transfer_on_mode_switch", True
),
"address_gap_limit": c.get("address_gap_limit", 20),
}
)
if name in ("monero", "wownero"):
@@ -203,6 +317,14 @@ def page_settings(self, url_split, post_string):
else:
chains_formatted[-1]["can_disable"] = True
try:
coin_id = swap_client.getCoinIdFromName(name)
lite_balance_info = swap_client.getLiteWalletBalanceInfo(coin_id)
if lite_balance_info:
chains_formatted[-1]["lite_wallet_balance"] = lite_balance_info
except Exception:
pass
general_settings = {
"debug": swap_client.debug,
"debug_ui": swap_client.debug_ui,

View File

@@ -32,11 +32,15 @@ DONATION_ADDRESSES = {
def format_wallet_data(swap_client, ci, w):
coin_id = ci.coin_type()
connection_type = swap_client.coin_clients.get(coin_id, {}).get(
"connection_type", w.get("connection_type", "rpc")
)
wf = {
"name": ci.coin_name(),
"version": w.get("version", "?"),
"ticker": ci.ticker_mainnet(),
"cid": str(int(ci.coin_type())),
"cid": str(int(coin_id)),
"balance": w.get("balance", "?"),
"blocks": w.get("blocks", "?"),
"synced": w.get("synced", "?"),
@@ -45,6 +49,7 @@ def format_wallet_data(swap_client, ci, w):
"locked": w.get("locked", "?"),
"updating": w.get("updating", "?"),
"havedata": True,
"connection_type": connection_type,
}
if "wallet_blocks" in w:
@@ -70,6 +75,9 @@ def format_wallet_data(swap_client, ci, w):
if pending > 0.0:
wf["pending"] = ci.format_amount(pending)
if "unconfirmed" in w and float(w["unconfirmed"]) < 0.0:
wf["pending_out"] = ci.format_amount(abs(ci.make_int(w["unconfirmed"])))
if ci.coin_type() == Coins.PART:
wf["stealth_address"] = w.get("stealth_address", "?")
wf["blind_balance"] = w.get("blind_balance", "?")
@@ -83,10 +91,85 @@ def format_wallet_data(swap_client, ci, w):
wf["mweb_balance"] = w.get("mweb_balance", "?")
wf["mweb_pending"] = w.get("mweb_pending", "?")
if hasattr(ci, "getScanStatus"):
wf["scan_status"] = ci.getScanStatus()
if connection_type == "electrum" and hasattr(ci, "_backend") and ci._backend:
backend = ci._backend
wf["electrum_server"] = backend.getServerHost()
wf["electrum_version"] = backend.getServerVersion()
try:
conn_status = backend.getConnectionStatus()
wf["electrum_connected"] = conn_status.get("connected", False)
wf["electrum_failures"] = conn_status.get("failures", 0)
wf["electrum_using_defaults"] = conn_status.get("using_defaults", True)
wf["electrum_all_failed"] = conn_status.get("all_failed", False)
wf["electrum_last_error"] = conn_status.get("last_error")
if conn_status.get("connected"):
wf["electrum_status"] = "connected"
elif conn_status.get("all_failed"):
wf["electrum_status"] = "all_failed"
else:
wf["electrum_status"] = "disconnected"
except Exception:
wf["electrum_connected"] = False
wf["electrum_status"] = "error"
checkAddressesOwned(swap_client, ci, wf)
return wf
def format_transactions(ci, transactions, coin_id):
formatted_txs = []
if coin_id in (Coins.XMR, Coins.WOW):
for tx in transactions:
tx_type = tx.get("type", "")
direction = (
"Incoming"
if tx_type == "in"
else "Outgoing" if tx_type == "out" else tx_type.capitalize()
)
formatted_txs.append(
{
"txid": tx.get("txid", ""),
"type": direction,
"amount": ci.format_amount(tx.get("amount", 0)),
"confirmations": tx.get("confirmations", 0),
"timestamp": format_timestamp(tx.get("timestamp", 0)),
"height": tx.get("height", 0),
}
)
else:
for tx in transactions:
category = tx.get("category", "")
if category == "send":
direction = "Outgoing"
amount = abs(tx.get("amount", 0))
elif category == "receive":
direction = "Incoming"
amount = tx.get("amount", 0)
else:
direction = category.capitalize()
amount = abs(tx.get("amount", 0))
formatted_txs.append(
{
"txid": tx.get("txid", ""),
"type": direction,
"amount": ci.format_amount(ci.make_int(amount)),
"confirmations": tx.get("confirmations", 0),
"timestamp": format_timestamp(
tx.get("time", tx.get("timereceived", 0))
),
"address": tx.get("address", ""),
}
)
return formatted_txs
def page_wallets(self, url_split, post_string):
server = self.server
swap_client = server.swap_client
@@ -131,6 +214,7 @@ def page_wallets(self, url_split, post_string):
"err_messages": err_messages,
"wallets": wallets_formatted,
"summary": summary,
"use_tor": getattr(swap_client, "use_tor_proxy", False),
},
)
@@ -151,8 +235,25 @@ def page_wallet(self, url_split, post_string):
show_utxo_groups: bool = False
withdrawal_successful: bool = False
force_refresh: bool = False
tx_filters = {
"page_no": 1,
"limit": 30,
"offset": 0,
}
form_data = self.checkForm(post_string, "wallet", err_messages)
if form_data:
if have_data_entry(form_data, "pageback"):
tx_filters["page_no"] = int(form_data[b"pageno"][0]) - 1
if tx_filters["page_no"] < 1:
tx_filters["page_no"] = 1
elif have_data_entry(form_data, "pageforwards"):
tx_filters["page_no"] = int(form_data[b"pageno"][0]) + 1
if tx_filters["page_no"] > 1:
tx_filters["offset"] = (tx_filters["page_no"] - 1) * 30
cid = str(int(coin_id))
estimate_fee: bool = have_data_entry(form_data, "estfee_" + cid)
@@ -170,6 +271,22 @@ def page_wallet(self, url_split, post_string):
except Exception as ex:
err_messages.append("Reseed failed " + str(ex))
swap_client.updateWalletsInfo(True, coin_id)
elif have_data_entry(form_data, "importkey_" + cid):
try:
wif_key = form_data[bytes("wifkey_" + cid, "utf-8")][0].decode("utf-8")
if wif_key:
result = swap_client.importWIFKey(coin_id, wif_key)
if result.get("success"):
messages.append(
f"Imported key for address: {result['address']}"
)
else:
err_messages.append(f"Import failed: {result.get('error')}")
else:
err_messages.append("Missing WIF key")
except Exception as ex:
err_messages.append(f"Import failed: {ex}")
swap_client.updateWalletsInfo(True, coin_id)
elif withdraw or estimate_fee:
subfee = True if have_data_entry(form_data, "subfee_" + cid) else False
page_data["wd_subfee_" + cid] = subfee
@@ -215,7 +332,14 @@ def page_wallet(self, url_split, post_string):
].decode("utf-8")
page_data["wd_type_from_" + cid] = type_from
except Exception as e: # noqa: F841
err_messages.append("Missing type")
if (
swap_client.coin_clients[coin_id].get("connection_type")
== "electrum"
):
type_from = "plain"
page_data["wd_type_from_" + cid] = type_from
else:
err_messages.append("Missing type")
if len(err_messages) == 0:
ci = swap_client.ci(coin_id)
@@ -328,6 +452,9 @@ def page_wallet(self, url_split, post_string):
cid = str(int(coin_id))
wallet_data = format_wallet_data(swap_client, ci, w)
wallet_data["is_electrum_mode"] = (
getattr(ci, "_connection_type", "rpc") == "electrum"
)
fee_rate, fee_src = swap_client.getFeeRateForCoin(k)
est_fee = swap_client.estimateWithdrawFee(k, fee_rate)
@@ -400,6 +527,26 @@ def page_wallet(self, url_split, post_string):
"coin_name": wallet_data.get("name", ticker),
}
transactions = []
total_transactions = 0
is_electrum_mode = False
if wallet_data.get("havedata", False) and not wallet_data.get("error"):
try:
ci = swap_client.ci(coin_id)
is_electrum_mode = getattr(ci, "_connection_type", "rpc") == "electrum"
if not is_electrum_mode:
count = tx_filters.get("limit", 30)
skip = tx_filters.get("offset", 0)
all_txs = ci.listWalletTransactions(count=10000, skip=0)
all_txs = list(reversed(all_txs)) if all_txs else []
total_transactions = len(all_txs)
raw_txs = all_txs[skip : skip + count] if all_txs else []
transactions = format_transactions(ci, raw_txs, coin_id)
except Exception as e:
swap_client.log.warning(f"Failed to fetch transactions for {ticker}: {e}")
template = server.env.get_template("wallet.html")
return self.render_template(
template,
@@ -411,5 +558,11 @@ def page_wallet(self, url_split, post_string):
"block_unknown_seeds": swap_client._restrict_unknown_seed_wallets,
"donation_info": donation_info,
"debug_ui": swap_client.debug_ui,
"transactions": transactions,
"tx_page_no": tx_filters.get("page_no", 1),
"tx_total": total_transactions,
"tx_limit": tx_filters.get("limit", 30),
"is_electrum_mode": is_electrum_mode,
"use_tor": getattr(swap_client, "use_tor_proxy", False),
},
)

View File

@@ -331,6 +331,7 @@ def describeBid(
"ticker_from": ci_from.ticker(),
"ticker_to": ci_to.ticker(),
"bid_state": strBidState(bid.state),
"bid_state_ind": int(bid.state),
"state_description": state_description,
"itx_state": strTxState(bid.getITxState()),
"ptx_state": strTxState(bid.getPTxState()),
@@ -343,6 +344,8 @@ def describeBid(
if for_api
else format_timestamp(bid.created_at, with_seconds=True)
),
"created_at_timestamp": bid.created_at,
"state_time_timestamp": getLastStateTimestamp(bid),
"expired_at": (
bid.expire_at
if for_api
@@ -623,6 +626,14 @@ def listOldBidStates(bid):
return old_states
def getLastStateTimestamp(bid):
if not bid.states or len(bid.states) < 12:
return None
num_states = len(bid.states) // 12
last_entry = struct.unpack_from("<iq", bid.states[(num_states - 1) * 12 :])
return last_entry[1]
def getCoinName(c):
if c == Coins.PART_ANON:
return chainparams[Coins.PART]["name"].capitalize() + " Anon"
@@ -643,7 +654,7 @@ def listAvailableCoins(swap_client, with_variants=True, split_from=False):
for k, v in swap_client.coin_clients.items():
if k not in chainparams:
continue
if v["connection_type"] == "rpc":
if v["connection_type"] in ("rpc", "electrum"):
coins.append((int(k), getCoinName(k)))
if split_from:
coins_from.append(coins[-1])
@@ -670,7 +681,7 @@ def listAvailableCoinsWithBalances(swap_client, with_variants=True, split_from=F
for k, v in swap_client.coin_clients.items():
if k not in chainparams:
continue
if v["connection_type"] == "rpc":
if v["connection_type"] in ("rpc", "electrum"):
balance = "0.0"
if k in wallets:
@@ -735,10 +746,23 @@ def checkAddressesOwned(swap_client, ci, wallet_info):
if wallet_info["stealth_address"] != "?":
if not ci.isAddressMine(wallet_info["stealth_address"]):
ci._log.error(
"Unowned stealth address: {}".format(wallet_info["stealth_address"])
ci._log.warning(
"Unowned stealth address: {} - clearing cache and regenerating".format(
wallet_info["stealth_address"]
)
)
wallet_info["stealth_address"] = "Error: unowned address"
key_str = "stealth_addr_" + ci.coin_name().lower()
swap_client.clearStringKV(key_str)
try:
new_addr = ci.getNewStealthAddress()
swap_client.setStringKV(key_str, new_addr)
wallet_info["stealth_address"] = new_addr
ci._log.info(
"Regenerated stealth address for {}".format(ci.coin_name())
)
except Exception as e:
ci._log.error("Failed to regenerate stealth address: {}".format(e))
wallet_info["stealth_address"] = "Error: unowned address"
elif (
swap_client._restrict_unknown_seed_wallets and not ci.knownWalletSeed()
):
@@ -747,10 +771,24 @@ def checkAddressesOwned(swap_client, ci, wallet_info):
if "deposit_address" in wallet_info:
if wallet_info["deposit_address"] != "Refresh necessary":
if not ci.isAddressMine(wallet_info["deposit_address"]):
ci._log.error(
"Unowned deposit address: {}".format(wallet_info["deposit_address"])
ci._log.warning(
"Unowned deposit address: {} - clearing cache and regenerating".format(
wallet_info["deposit_address"]
)
)
wallet_info["deposit_address"] = "Error: unowned address"
key_str = "receive_addr_" + ci.coin_name().lower()
swap_client.clearStringKV(key_str)
try:
coin_type = ci.coin_type()
new_addr = swap_client.getReceiveAddressForCoin(coin_type)
swap_client.setStringKV(key_str, new_addr)
wallet_info["deposit_address"] = new_addr
ci._log.info(
"Regenerated deposit address for {}".format(ci.coin_name())
)
except Exception as e:
ci._log.error("Failed to regenerate deposit address: {}".format(e))
wallet_info["deposit_address"] = "Error: unowned address"
elif (
swap_client._restrict_unknown_seed_wallets and not ci.knownWalletSeed()
):