This commit is contained in:
gerlofvanek
2026-02-06 21:39:17 +01:00
parent c9029a5e34
commit 7b0925de46
8 changed files with 648 additions and 69 deletions

View File

@@ -2428,13 +2428,6 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
def _syncLiteWalletToRPCOnStartup(self, coin_type: Coins) -> None: def _syncLiteWalletToRPCOnStartup(self, coin_type: Coins) -> None:
try: try:
cc = self.coin_clients[coin_type]
if not cc.get("auto_transfer_on_mode_switch", True):
self.log.debug(
f"Auto-transfer disabled for {getCoinName(coin_type)}, skipping sweep check"
)
return
cursor = self.openDB() cursor = self.openDB()
try: try:
row = cursor.execute( row = cursor.execute(
@@ -2780,6 +2773,137 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
self.log.error(f"_transferLiteWalletBalanceToRPC error: {e}") self.log.error(f"_transferLiteWalletBalanceToRPC error: {e}")
return None return None
def sweepLiteWalletFunds(self, coin_type: Coins) -> dict:
try:
coin_name = getCoinName(coin_type)
self.log.info(f"Manual sweep requested for {coin_name}")
if coin_type in WalletManager.SUPPORTED_COINS:
if not self._wallet_manager.isInitialized(coin_type):
try:
self.initializeWalletManager(coin_type)
except Exception as e:
return {
"success": False,
"error": f"Wallet locked: {e}",
}
result = self._transferLiteWalletBalanceToRPC(coin_type)
if result is None:
return {"skipped": True, "reason": "No balance to sweep"}
if result.get("skipped"):
return result
if result.get("txid"):
return {
"success": True,
"txid": result["txid"],
"amount": result.get("amount", 0) / 1e8,
"fee": result.get("fee", 0) / 1e8,
"address": result.get("address", ""),
}
if result.get("error"):
return {"success": False, "error": result["error"]}
return {"skipped": True, "reason": "Unknown result"}
except Exception as e:
self.log.error(
f"sweepLiteWalletFunds error for {getCoinName(coin_type)}: {e}"
)
return {"success": False, "error": str(e)}
def _consolidateLegacyFundsToSegwit(self, coin_type: Coins) -> dict:
try:
coin_name = getCoinName(coin_type)
cc = self.coin_clients[coin_type]
ci = cc.get("interface")
if not ci:
return {"skipped": True, "reason": "No coin interface"}
try:
unspent = ci.rpc_wallet("listunspent")
except Exception as e:
return {"error": f"Failed to list UTXOs: {e}"}
hrp = ci.chainparams_network().get("hrp", "bc")
legacy_utxos = []
total_legacy_sats = 0
for u in unspent:
if "address" not in u:
continue
addr = u["address"]
if not addr.startswith(hrp + "1"):
legacy_utxos.append(u)
total_legacy_sats += ci.make_int(u.get("amount", 0))
if not legacy_utxos:
return {"skipped": True, "reason": "No legacy funds found"}
if total_legacy_sats <= 0:
return {"skipped": True, "reason": "No balance on legacy addresses"}
est_vsize = len(legacy_utxos) * 150 + 40
fee_rate, _ = ci.get_fee_rate(ci._conf_target)
if isinstance(fee_rate, int):
fee_per_vbyte = max(1, fee_rate // 1000)
else:
fee_per_vbyte = max(1, int(fee_rate * 100000))
estimated_fee_sats = est_vsize * fee_per_vbyte
if total_legacy_sats <= estimated_fee_sats * 2:
return {
"skipped": True,
"reason": f"Legacy balance ({total_legacy_sats}) too low for fee ({estimated_fee_sats})",
}
try:
new_address = ci.rpc_wallet("getnewaddress", ["consolidate", "bech32"])
except Exception as e:
return {"error": f"Failed to get new address: {e}"}
send_amount_sats = total_legacy_sats - estimated_fee_sats
send_amount_btc = ci.format_amount(send_amount_sats)
self.log.info(
f"[Consolidate {coin_name}] Moving {ci.format_amount(total_legacy_sats)} from "
f"{len(legacy_utxos)} legacy UTXOs to {new_address}"
)
inputs = [{"txid": u["txid"], "vout": u["vout"]} for u in legacy_utxos]
try:
raw_tx = ci.rpc_wallet(
"createrawtransaction",
[inputs, {new_address: float(send_amount_btc)}],
)
except Exception as e:
return {"error": f"Failed to create transaction: {e}"}
try:
signed = ci.rpc_wallet("signrawtransactionwithwallet", [raw_tx])
if not signed.get("complete"):
return {"error": "Failed to sign transaction"}
txid = ci.rpc_wallet("sendrawtransaction", [signed["hex"]])
except Exception as e:
return {"error": f"Failed to broadcast transaction: {e}"}
self.log.info(f"[Consolidate {coin_name}] SUCCESS! TXID: {txid}")
return {
"success": True,
"txid": txid,
"amount": send_amount_sats / 1e8,
"fee": estimated_fee_sats / 1e8,
"address": new_address,
"num_inputs": len(legacy_utxos),
}
except Exception as e:
self.log.error(f"_consolidateLegacyFundsToSegwit error: {e}")
import traceback
self.log.debug(traceback.format_exc())
return {"error": str(e)}
def _retryPendingSweeps(self) -> None: def _retryPendingSweeps(self) -> None:
if not self._pending_sweeps: if not self._pending_sweeps:
return return
@@ -2869,6 +2993,43 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
self.log.debug(f"getLiteWalletBalanceInfo error: {e}") self.log.debug(f"getLiteWalletBalanceInfo error: {e}")
return None return None
def getElectrumLegacyFundsInfo(self, coin_type: Coins) -> dict:
try:
cc = self.coin_clients.get(coin_type)
if not cc or cc.get("connection_type") != "electrum":
return {"has_legacy_funds": False}
if not self._wallet_manager:
return {"has_legacy_funds": False}
ci = self.ci(coin_type)
hrp = ci.chainparams_network().get("hrp", "bc")
unspent_by_addr = ci.getUnspentsByAddr()
if not unspent_by_addr:
return {"has_legacy_funds": False}
legacy_balance_sats = 0
legacy_addresses = []
for addr, balance_sats in unspent_by_addr.items():
if not addr.startswith(hrp + "1"):
legacy_balance_sats += balance_sats
legacy_addresses.append(addr)
if legacy_balance_sats > 0:
return {
"has_legacy_funds": True,
"legacy_balance_sats": legacy_balance_sats,
"legacy_balance": ci.format_amount(legacy_balance_sats),
"legacy_address_count": len(legacy_addresses),
"coin": ci.ticker_mainnet(),
}
return {"has_legacy_funds": False}
except Exception as e:
self.log.debug(f"getElectrumLegacyFundsInfo error: {e}")
return {"has_legacy_funds": False}
def _tryGetFullNodeAddresses(self, coin_type: Coins) -> list: def _tryGetFullNodeAddresses(self, coin_type: Coins) -> list:
addresses = [] addresses = []
try: try:
@@ -13089,15 +13250,39 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
display_name = getCoinName(coin_id) display_name = getCoinName(coin_id)
if old_connection_type == "rpc" and new_connection_type == "electrum": if old_connection_type == "rpc" and new_connection_type == "electrum":
auto_transfer_now = data.get("auto_transfer_now", False)
if auto_transfer_now:
transfer_result = self._consolidateLegacyFundsToSegwit(coin_id)
if transfer_result.get("success"):
self.log.info(
f"Consolidated {transfer_result.get('amount', 0):.8f} {display_name} "
f"from legacy addresses. TXID: {transfer_result.get('txid')}"
)
if migration_message:
migration_message += f" Transferred {transfer_result.get('amount', 0):.8f} {display_name} to native segwit."
else:
migration_message = f"Transferred {transfer_result.get('amount', 0):.8f} {display_name} to native segwit."
elif transfer_result.get("skipped"):
self.log.info(
f"Legacy fund transfer skipped for {coin_name}: {transfer_result.get('reason')}"
)
elif transfer_result.get("error"):
self.log.warning(
f"Legacy fund transfer warning for {coin_name}: {transfer_result.get('error')}"
)
migration_result = self._migrateWalletToLiteMode(coin_id) migration_result = self._migrateWalletToLiteMode(coin_id)
if migration_result.get("success"): if migration_result.get("success"):
count = migration_result.get("count", 0) count = migration_result.get("count", 0)
self.log.info( self.log.info(
f"Lite wallet ready for {coin_name} with {count} addresses" f"Lite wallet ready for {coin_name} with {count} addresses"
) )
migration_message = ( if migration_message:
f"Lite wallet ready for {display_name} ({count} addresses)." migration_message += (
f" Lite wallet ready ({count} addresses)."
) )
else:
migration_message = f"Lite wallet ready for {display_name} ({count} addresses)."
else: else:
error = migration_result.get("error", "unknown") error = migration_result.get("error", "unknown")
reason = migration_result.get("reason", "") reason = migration_result.get("reason", "")
@@ -13120,11 +13305,38 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
empty_check = self._checkElectrumWalletEmpty(coin_id) empty_check = self._checkElectrumWalletEmpty(coin_id)
if not empty_check.get("empty", False): if not empty_check.get("empty", False):
reason = empty_check.get("reason", "")
auto_transfer_now = data.get("auto_transfer_now", False)
if reason == "has_balance" and auto_transfer_now:
self.log.info(
f"Auto-transfer requested for {coin_name} during mode switch"
)
sweep_result = self.sweepLiteWalletFunds(coin_id)
if sweep_result.get("success"):
self.log.info(
f"Swept {sweep_result.get('amount', 0):.8f} {display_name} "
f"to RPC wallet. TXID: {sweep_result.get('txid')}"
)
migration_message = (
f"Transferred {sweep_result.get('amount', 0):.8f} {display_name} "
f"to full node wallet."
)
elif sweep_result.get("skipped"):
self.log.info(
f"Sweep skipped for {coin_name}: {sweep_result.get('reason')}"
)
else:
error = sweep_result.get("error", "Transfer failed")
self.log.error(
f"Transfer failed for {coin_name}: {error}"
)
raise ValueError(f"Transfer failed: {error}")
elif reason in ("has_balance", "active_swap"):
error = empty_check.get( error = empty_check.get(
"message", "Wallet must be empty before switching modes" "message", "Wallet must be empty before switching modes"
) )
reason = empty_check.get("reason", "")
if reason in ("has_balance", "active_swap"):
self.log.error( self.log.error(
f"Migration blocked for {coin_name}: {error}" f"Migration blocked for {coin_name}: {error}"
) )
@@ -13281,19 +13493,6 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
cc["electrum_onion_servers"] = new_onion cc["electrum_onion_servers"] = new_onion
break break
if "auto_transfer_on_mode_switch" in data:
new_auto_transfer = data["auto_transfer_on_mode_switch"]
if (
settings_cc.get("auto_transfer_on_mode_switch", True)
!= new_auto_transfer
):
settings_changed = True
settings_cc["auto_transfer_on_mode_switch"] = new_auto_transfer
for coin, cc in self.coin_clients.items():
if cc["name"] == coin_name:
cc["auto_transfer_on_mode_switch"] = new_auto_transfer
break
if settings_changed: if settings_changed:
self._normalizeSettingsPaths(settings_copy) self._normalizeSettingsPaths(settings_copy)
settings_path = os.path.join(self.data_dir, cfg.CONFIG_FILENAME) settings_path = os.path.join(self.data_dir, cfg.CONFIG_FILENAME)

View File

@@ -1841,6 +1841,7 @@ def initialise_wallets(
daemons = [] daemons = []
daemon_args = ["-noconnect", "-nodnsseed"] daemon_args = ["-noconnect", "-nodnsseed"]
generated_mnemonic: bool = False generated_mnemonic: bool = False
extended_keys = {}
coins_failed_to_initialise = [] coins_failed_to_initialise = []
@@ -2098,6 +2099,20 @@ def initialise_wallets(
except Exception as e: # noqa: F841 except Exception as e: # noqa: F841
logger.warning(f"changeWalletPassword failed for {coin_name}.") logger.warning(f"changeWalletPassword failed for {coin_name}.")
zprv_prefix = 0x04B2430C if chain == "mainnet" else 0x045F18BC
for coin_name in with_coins:
c = swap_client.getCoinIdFromName(coin_name)
if c == Coins.PART:
continue
try:
ci = swap_client.ci(c)
if hasattr(ci, "canExportToElectrum") and ci.canExportToElectrum():
seed_key = swap_client.getWalletKey(c, 1)
account_key = ci.getAccountKey(seed_key, zprv_prefix)
extended_keys[getCoinName(c)] = account_key
except Exception as e:
logger.debug(f"Could not generate extended key for {coin_name}: {e}")
finally: finally:
if swap_client: if swap_client:
swap_client.finalise() swap_client.finalise()
@@ -2129,6 +2144,18 @@ def initialise_wallets(
) )
) )
if extended_keys:
print("Extended private keys (for external wallet import):")
for coin_name, key in extended_keys.items():
print(f" {coin_name}: {key}")
print("")
print(
"NOTE: These keys can be imported into Electrum using 'Use a master key'."
)
print("WARNING: Write these down NOW. They will not be shown again.\n")
return extended_keys
def load_config(config_path): def load_config(config_path):
if not os.path.exists(config_path): if not os.path.exists(config_path):
@@ -3071,7 +3098,7 @@ def main():
) )
if particl_wallet_mnemonic != "none": if particl_wallet_mnemonic != "none":
initialise_wallets( extended_keys = initialise_wallets(
None, None,
{ {
add_coin, add_coin,
@@ -3083,6 +3110,18 @@ def main():
extra_opts=extra_opts, extra_opts=extra_opts,
) )
if extended_keys:
print("\nExtended private key (for external wallet import):")
for coin_name, key in extended_keys.items():
print(f" {coin_name}: {key}")
print("")
print(
"NOTE: This key can be imported into Electrum using 'Use a master key'."
)
print(
"WARNING: Write this down NOW. It will not be shown again.\n"
)
save_config(config_path, settings) save_config(config_path, settings)
finally: finally:
if "particl_daemon" in extra_opts: if "particl_daemon" in extra_opts:

View File

@@ -3355,7 +3355,8 @@ class BTCInterface(Secp256k1Interface):
total_balance = sum(u.get("value", 0) for u in all_utxos) total_balance = sum(u.get("value", 0) for u in all_utxos)
est_vsize = 10 + 31 + len(all_utxos) * 68 est_vsize = 10 + 31 + len(all_utxos) * 68
est_fee = est_vsize * fee_per_vbyte min_fee = 250
est_fee = max(est_vsize * fee_per_vbyte, min_fee)
if total_balance <= est_fee: if total_balance <= est_fee:
raise ValueError( raise ValueError(

View File

@@ -1248,10 +1248,12 @@ def js_getcoinseed(self, url_split, post_string, is_json) -> bytes:
"current_seed_id": wallet_seed_id, "current_seed_id": wallet_seed_id,
} }
) )
if hasattr(ci, "canExportToElectrum") and ci.canExportToElectrum():
rv.update( if hasattr(ci, "getAccountKey"):
{"account_key": ci.getAccountKey(seed_key, extkey_prefix)} try:
) # Master key can be imported into electrum (Must set prefix for P2WPKH) rv.update({"account_key": ci.getAccountKey(seed_key, extkey_prefix)})
except Exception as e:
rv.update({"account_key_error": str(e)})
return bytes( return bytes(
json.dumps(rv), json.dumps(rv),
@@ -1674,6 +1676,97 @@ def js_messageroutes(self, url_split, post_string, is_json) -> bytes:
return bytes(json.dumps(message_routes), "UTF-8") return bytes(json.dumps(message_routes), "UTF-8")
def js_modeswitchinfo(self, url_split, post_string, is_json) -> bytes:
swap_client = self.server.swap_client
swap_client.checkSystemStatus()
post_data = getFormData(post_string, is_json)
coin_str = get_data_entry(post_data, "coin")
direction = get_data_entry_or(post_data, "direction", "lite")
try:
coin_type = getCoinIdFromName(coin_str)
except Exception:
coin_type = getCoinIdFromTicker(coin_str.upper())
ci = swap_client.ci(coin_type)
ticker = ci.ticker()
try:
wallet_info = ci.getWalletInfo()
balance = wallet_info.get("balance", 0)
balance_sats = ci.make_int(balance)
except Exception as e:
return bytes(json.dumps({"error": f"Failed to get balance: {e}"}), "UTF-8")
try:
fee_rate, rate_src = ci.get_fee_rate(ci._conf_target)
est_vsize = 180
if isinstance(fee_rate, int):
fee_per_vbyte = max(1, fee_rate // 1000)
else:
fee_per_vbyte = max(1, int(fee_rate * 100000))
estimated_fee_sats = est_vsize * fee_per_vbyte
except Exception:
estimated_fee_sats = 180
rate_src = "default"
min_viable = estimated_fee_sats * 2
can_transfer = balance_sats > min_viable
rv = {
"coin": ticker,
"direction": direction,
"balance": balance,
"balance_sats": balance_sats,
"estimated_fee_sats": estimated_fee_sats,
"estimated_fee": ci.format_amount(estimated_fee_sats),
"fee_rate_src": rate_src,
"can_transfer": can_transfer,
"min_viable_sats": min_viable,
}
if direction == "lite":
legacy_balance_sats = 0
has_legacy_funds = False
try:
if hasattr(ci, "rpc_wallet"):
unspent = ci.rpc_wallet("listunspent")
hrp = ci.chainparams_network().get("hrp", "bc")
for u in unspent:
if "address" in u and not u["address"].startswith(hrp + "1"):
legacy_balance_sats += ci.make_int(u.get("amount", 0))
has_legacy_funds = True
except Exception as e:
swap_client.log.debug(f"Error checking legacy addresses: {e}")
if has_legacy_funds and legacy_balance_sats > min_viable:
rv["show_transfer_option"] = True
rv["legacy_balance_sats"] = legacy_balance_sats
rv["legacy_balance"] = ci.format_amount(legacy_balance_sats)
rv["message"] = (
"Funds on legacy addresses - transfer recommended for external wallet compatibility"
)
else:
rv["show_transfer_option"] = False
rv["legacy_balance_sats"] = 0
rv["legacy_balance"] = "0"
if has_legacy_funds:
rv["message"] = "Legacy balance too low to transfer"
else:
rv["message"] = "All funds on native segwit addresses"
else:
rv["show_transfer_option"] = can_transfer
if balance_sats == 0:
rv["message"] = "No funds to transfer"
elif not can_transfer:
rv["message"] = "Balance too low to transfer (fee would exceed funds)"
else:
rv["message"] = ""
return bytes(json.dumps(rv), "UTF-8")
def js_electrum_discover(self, url_split, post_string, is_json) -> bytes: def js_electrum_discover(self, url_split, post_string, is_json) -> bytes:
swap_client = self.server.swap_client swap_client = self.server.swap_client
post_data = {} if post_string == "" else getFormData(post_string, is_json) post_data = {} if post_string == "" else getFormData(post_string, is_json)
@@ -1805,6 +1898,7 @@ endpoints = {
"coinhistory": js_coinhistory, "coinhistory": js_coinhistory,
"messageroutes": js_messageroutes, "messageroutes": js_messageroutes,
"electrumdiscover": js_electrum_discover, "electrumdiscover": js_electrum_discover,
"modeswitchinfo": js_modeswitchinfo,
} }

View File

@@ -147,6 +147,16 @@
hiddenInput.name = submitter.name; hiddenInput.name = submitter.name;
hiddenInput.value = submitter.value; hiddenInput.value = submitter.value;
form.appendChild(hiddenInput); form.appendChild(hiddenInput);
const transferRadio = document.querySelector('input[name="transfer_choice"]:checked');
if (transferRadio) {
const transferInput = document.createElement('input');
transferInput.type = 'hidden';
transferInput.name = `auto_transfer_now_${coinName}`;
transferInput.value = transferRadio.value === 'auto' ? 'true' : 'false';
form.appendChild(transferInput);
}
form.submit(); form.submit();
} }
}); });
@@ -167,11 +177,26 @@
} }
}, },
showWalletModeConfirmation: function(coinName, direction, submitter) { updateConfirmButtonState: function() {
const confirmBtn = document.getElementById('walletModeConfirm');
const checkbox = document.getElementById('walletModeKeyConfirmCheckbox');
if (confirmBtn && checkbox) {
if (checkbox.checked) {
confirmBtn.disabled = false;
confirmBtn.classList.remove('opacity-50', 'cursor-not-allowed');
} else {
confirmBtn.disabled = true;
confirmBtn.classList.add('opacity-50', 'cursor-not-allowed');
}
}
},
showWalletModeConfirmation: async function(coinName, direction, submitter) {
const modal = document.getElementById('walletModeModal'); const modal = document.getElementById('walletModeModal');
const title = document.getElementById('walletModeTitle'); const title = document.getElementById('walletModeTitle');
const message = document.getElementById('walletModeMessage'); const message = document.getElementById('walletModeMessage');
const details = document.getElementById('walletModeDetails'); const details = document.getElementById('walletModeDetails');
const confirmBtn = document.getElementById('walletModeConfirm');
if (!modal || !title || !message || !details) return; if (!modal || !title || !message || !details) return;
@@ -179,37 +204,223 @@
const displayName = coinName.charAt(0).toUpperCase() + coinName.slice(1).toLowerCase(); const displayName = coinName.charAt(0).toUpperCase() + coinName.slice(1).toLowerCase();
details.innerHTML = `
<div class="flex items-center justify-center py-4">
<svg class="animate-spin h-5 w-5 text-blue-500 mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<span>Loading...</span>
</div>
`;
if (confirmBtn) {
confirmBtn.disabled = true;
confirmBtn.classList.add('opacity-50', 'cursor-not-allowed');
}
modal.classList.remove('hidden');
if (direction === 'lite') { if (direction === 'lite') {
title.textContent = `Switch ${displayName} to Lite Wallet Mode`; title.textContent = `Switch ${displayName} to Lite Wallet Mode`;
message.textContent = 'Please confirm you want to switch to lite wallet mode.'; message.textContent = 'Write down this key before switching. It will only be shown ONCE.';
try {
const [infoResponse, seedResponse] = await Promise.all([
fetch('/json/modeswitchinfo', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ coin: coinName, direction: 'lite' })
}),
fetch('/json/getcoinseed', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ coin: coinName })
})
]);
const info = await infoResponse.json();
const data = await seedResponse.json();
let transferSection = '';
if (info.show_transfer_option && info.legacy_balance_sats > 0) {
transferSection = `
<div class="bg-gray-200 dark:bg-gray-700 border border-gray-300 dark:border-gray-500 rounded-lg p-3 mb-3">
<p class="text-sm font-medium text-yellow-700 dark:text-yellow-300 mb-2">Legacy Address Funds Detected</p>
<p class="text-xs text-gray-700 dark:text-gray-300 mb-3">
${info.legacy_balance} ${info.coin} on legacy addresses won't be visible in external Electrum wallet.
Est. fee: ${info.estimated_fee} ${info.coin}
</p>
<div class="space-y-2">
<label class="flex items-start cursor-pointer hover:bg-gray-300 dark:hover:bg-gray-600 rounded p-1.5 -m-1">
<input type="radio" name="transfer_choice" value="auto" class="mt-0.5 mr-2 h-4 w-4 text-blue-600 border-gray-400 dark:border-gray-400 focus:ring-blue-500 bg-white dark:bg-gray-500">
<div>
<span class="text-sm font-medium text-gray-900 dark:text-white">Transfer to native segwit address</span>
<p class="text-xs text-gray-600 dark:text-gray-300">Recommended for external wallet compatibility.</p>
</div>
</label>
<label class="flex items-start cursor-pointer hover:bg-gray-300 dark:hover:bg-gray-600 rounded p-1.5 -m-1">
<input type="radio" name="transfer_choice" value="manual" checked class="mt-0.5 mr-2 h-4 w-4 text-blue-600 border-gray-400 dark:border-gray-400 focus:ring-blue-500 bg-white dark:bg-gray-500">
<div>
<span class="text-sm font-medium text-gray-900 dark:text-white">Keep on current addresses</span>
<p class="text-xs text-gray-600 dark:text-gray-300">Funds accessible in BasicSwap, transfer manually later if needed.</p>
</div>
</label>
</div>
<p class="text-xs text-red-600 dark:text-red-400 mt-3">
If you skip transfer, legacy funds won't be visible when importing the extended key into external Electrum wallet.
</p>
</div>
`;
} else if (info.legacy_balance_sats > 0 && !info.show_transfer_option) {
transferSection = `
<p class="text-yellow-700 dark:text-yellow-300 text-xs mb-3">
Some funds on legacy addresses (${info.legacy_balance} ${info.coin}) - too low to transfer.
</p>
`;
}
if (data.account_key) {
details.innerHTML = ` details.innerHTML = `
<p class="mb-2"><strong>Before switching:</strong></p> <p class="mb-2 text-red-600 dark:text-red-300 font-semibold">
<ul class="list-disc list-inside space-y-1"> IMPORTANT: Write down this key NOW. It will not be shown again.
</p>
<p class="mb-2 text-gray-800 dark:text-gray-100"><strong>Extended Private Key (for external wallet import):</strong></p>
<div class="bg-gray-50 dark:bg-gray-700 border border-gray-300 dark:border-gray-500 rounded p-2 mb-3">
<code class="text-xs break-all select-all font-mono text-gray-900 dark:text-gray-100">${data.account_key}</code>
</div>
<p class="text-xs text-gray-600 dark:text-gray-300 mb-3">
This key can be imported into Electrum using "Use a master key" option.
</p>
${transferSection}
<div class="border-t border-gray-300 dark:border-gray-500 pt-3">
<label class="flex items-center cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-500 rounded p-1 -m-1">
<input type="checkbox" id="walletModeKeyConfirmCheckbox" class="mr-2 h-4 w-4 text-blue-600 rounded border-gray-300 dark:border-gray-500 focus:ring-blue-500 dark:bg-gray-700">
<span class="text-sm font-medium text-gray-800 dark:text-gray-100">I have written down this key</span>
</label>
</div>
`;
const checkbox = document.getElementById('walletModeKeyConfirmCheckbox');
if (checkbox) {
checkbox.addEventListener('change', () => this.updateConfirmButtonState());
}
} else {
details.innerHTML = `
<p class="mb-2 text-gray-800 dark:text-gray-100"><strong>Before switching:</strong></p>
<ul class="list-disc list-inside space-y-1 text-gray-700 dark:text-gray-200">
<li>Active swaps must be completed first</li> <li>Active swaps must be completed first</li>
<li>Wait for any pending transactions to confirm</li> <li>Wait for any pending transactions to confirm</li>
</ul> </ul>
<p class="mt-3 text-green-600 dark:text-green-400"> ${transferSection}
<p class="mt-3 text-green-700 dark:text-green-300">
<strong>Note:</strong> Your balance will remain accessible - same seed means same funds in both modes. <strong>Note:</strong> Your balance will remain accessible - same seed means same funds in both modes.
</p> </p>
`; `;
if (confirmBtn) {
confirmBtn.disabled = false;
confirmBtn.classList.remove('opacity-50', 'cursor-not-allowed');
}
}
} catch (error) {
console.error('Failed to fetch coin seed:', error);
details.innerHTML = `
<p class="text-red-600 dark:text-red-300 mb-2">Failed to retrieve extended key. Please try again.</p>
<p class="mb-2 text-gray-800 dark:text-gray-100"><strong>Before switching:</strong></p>
<ul class="list-disc list-inside space-y-1 text-gray-700 dark:text-gray-200">
<li>Active swaps must be completed first</li>
<li>Wait for any pending transactions to confirm</li>
</ul>
`;
if (confirmBtn) {
confirmBtn.disabled = false;
confirmBtn.classList.remove('opacity-50', 'cursor-not-allowed');
}
}
} else { } else {
title.textContent = `Switch ${displayName} to Full Node Mode`; title.textContent = `Switch ${displayName} to Full Node Mode`;
message.textContent = 'Please confirm you want to switch to full node mode.'; message.textContent = 'Please confirm you want to switch to full node mode.';
try {
const response = await fetch('/json/modeswitchinfo', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ coin: coinName, direction: 'rpc' })
});
const info = await response.json();
let transferSection = '';
if (info.error) {
transferSection = `<p class="text-yellow-700 dark:text-yellow-300 text-sm">${info.error}</p>`;
} else if (info.balance_sats === 0) {
transferSection = `<p class="text-gray-600 dark:text-gray-300 text-sm">No funds to transfer.</p>`;
} else if (!info.can_transfer) {
transferSection = `
<p class="text-yellow-700 dark:text-yellow-300 text-sm">
Balance (${info.balance} ${info.coin}) is too low to transfer - fee would exceed funds.
</p>
`;
} else {
transferSection = `
<div class="bg-gray-200 dark:bg-gray-700 border border-gray-300 dark:border-gray-500 rounded-lg p-3 mb-3">
<p class="text-sm font-medium text-gray-900 dark:text-white mb-2">Fund Transfer Options</p>
<p class="text-xs text-gray-700 dark:text-gray-300 mb-3">
Balance: ${info.balance} ${info.coin} | Est. fee: ${info.estimated_fee} ${info.coin}
</p>
<div class="space-y-2">
<label class="flex items-start cursor-pointer hover:bg-gray-300 dark:hover:bg-gray-600 rounded p-1.5 -m-1">
<input type="radio" name="transfer_choice" value="auto" class="mt-0.5 mr-2 h-4 w-4 text-blue-600 border-gray-400 dark:border-gray-400 focus:ring-blue-500 bg-white dark:bg-gray-500">
<div>
<span class="text-sm font-medium text-gray-900 dark:text-white">Auto-transfer funds to RPC wallet</span>
<p class="text-xs text-gray-600 dark:text-gray-300">Recommended. Ensures all funds visible in full node wallet.</p>
</div>
</label>
<label class="flex items-start cursor-pointer hover:bg-gray-300 dark:hover:bg-gray-600 rounded p-1.5 -m-1">
<input type="radio" name="transfer_choice" value="manual" checked class="mt-0.5 mr-2 h-4 w-4 text-blue-600 border-gray-400 dark:border-gray-400 focus:ring-blue-500 bg-white dark:bg-gray-500">
<div>
<span class="text-sm font-medium text-gray-900 dark:text-white">Keep funds on current addresses</span>
<p class="text-xs text-gray-600 dark:text-gray-300">Transfer manually later if needed.</p>
</div>
</label>
</div>
<p class="text-xs text-red-600 dark:text-red-400 mt-3">
If you skip transfer, you will need to manually send funds from lite wallet addresses to your RPC wallet.
</p>
</div>
`;
}
details.innerHTML = ` details.innerHTML = `
<p class="mb-2"><strong>Switching to full node mode:</strong></p> <p class="mb-2 text-gray-800 dark:text-gray-100"><strong>Switching to full node mode:</strong></p>
<ul class="list-disc list-inside space-y-1"> <ul class="list-disc list-inside space-y-1 mb-3 text-gray-700 dark:text-gray-200">
<li>Requires synced ${displayName} blockchain</li> <li>Requires synced ${displayName} blockchain</li>
<li>Your wallet addresses will be synced</li> <li>Your wallet addresses will be synced</li>
<li>Active swaps must be completed first</li> <li>Active swaps must be completed first</li>
<li>Restart required after switch</li> <li>Restart required after switch</li>
</ul> </ul>
<p class="mt-3 text-green-600 dark:text-green-400"> ${transferSection}
<strong>Note:</strong> Your balance will remain accessible - same seed means same funds in both modes.
</p>
`; `;
}
modal.classList.remove('hidden'); if (confirmBtn) {
confirmBtn.disabled = false;
confirmBtn.classList.remove('opacity-50', 'cursor-not-allowed');
}
} catch (error) {
console.error('Failed to fetch mode switch info:', error);
details.innerHTML = `
<p class="mb-2 text-gray-800 dark:text-gray-100"><strong>Switching to full node mode:</strong></p>
<ul class="list-disc list-inside space-y-1 text-gray-700 dark:text-gray-200">
<li>Requires synced ${displayName} blockchain</li>
<li>Your wallet addresses will be synced</li>
<li>Active swaps must be completed first</li>
<li>Restart required after switch</li>
</ul>
`;
if (confirmBtn) {
confirmBtn.disabled = false;
confirmBtn.classList.remove('opacity-50', 'cursor-not-allowed');
}
}
}
}, },
hideWalletModeModal: function() { hideWalletModeModal: function() {

View File

@@ -82,6 +82,36 @@
</section> </section>
{% endif %} {% endif %}
{% if legacy_funds_info and legacy_funds_info.has_legacy_funds %}
<section class="py-4 px-6" id="legacy_funds_warning">
<div class="lg:container mx-auto">
<div class="p-6 rounded-lg bg-yellow-50 border border-yellow-400 dark:bg-yellow-900/30 dark:border-yellow-700">
<div class="flex flex-wrap justify-between items-center -m-2">
<div class="flex-1 p-2">
<div class="flex flex-wrap -m-1">
<div class="w-auto p-1">
<svg class="w-6 h-6 text-yellow-600 dark:text-yellow-400" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/>
</svg>
</div>
<div class="ml-3 flex-1">
<p class="font-semibold text-lg lg:text-sm text-yellow-700 dark:text-yellow-300">Legacy Address Funds</p>
<p class="mt-1 text-sm text-yellow-600 dark:text-yellow-400">
{{ legacy_funds_info.legacy_balance }} {{ legacy_funds_info.coin }} on legacy addresses won't be visible in external Electrum wallet.
To use funds with external wallets, transfer to a new address.
</p>
<p class="mt-2 text-xs text-yellow-500 dark:text-yellow-500">
Use the withdraw function below to send funds to a new <code class="bg-yellow-100 dark:bg-yellow-800/50 px-1 rounded">{{ w.ticker | lower }}1...</code> address.
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
{% endif %}
<section> <section>
<form method="post" autocomplete="off"> <form method="post" autocomplete="off">
<div class="px-6 py-0 h-full overflow-hidden"> <div class="px-6 py-0 h-full overflow-hidden">

View File

@@ -180,11 +180,14 @@ def page_settings(self, url_split, post_string):
form_data, "electrum_onion_" + name, "" form_data, "electrum_onion_" + name, ""
).strip() ).strip()
data["electrum_onion_servers"] = onion_servers data["electrum_onion_servers"] = onion_servers
auto_transfer = have_data_entry( auto_transfer_now = have_data_entry(
form_data, "auto_transfer_" + name form_data, "auto_transfer_now_" + name
) )
data["auto_transfer_on_mode_switch"] = auto_transfer if auto_transfer_now:
# Address gap limit for scanning transfer_value = get_data_entry_or(
form_data, "auto_transfer_now_" + name, "false"
)
data["auto_transfer_now"] = transfer_value == "true"
gap_limit_str = get_data_entry_or( gap_limit_str = get_data_entry_or(
form_data, "gap_limit_" + name, "20" form_data, "gap_limit_" + name, "20"
).strip() ).strip()
@@ -292,9 +295,6 @@ def page_settings(self, url_split, post_string):
"supports_electrum": name in electrum_supported_coins, "supports_electrum": name in electrum_supported_coins,
"clearnet_servers_text": clearnet_text, "clearnet_servers_text": clearnet_text,
"onion_servers_text": onion_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), "address_gap_limit": c.get("address_gap_limit", 20),
} }
) )

View File

@@ -530,6 +530,7 @@ def page_wallet(self, url_split, post_string):
transactions = [] transactions = []
total_transactions = 0 total_transactions = 0
is_electrum_mode = False is_electrum_mode = False
legacy_funds_info = None
if wallet_data.get("havedata", False) and not wallet_data.get("error"): if wallet_data.get("havedata", False) and not wallet_data.get("error"):
try: try:
ci = swap_client.ci(coin_id) ci = swap_client.ci(coin_id)
@@ -544,6 +545,9 @@ def page_wallet(self, url_split, post_string):
raw_txs = all_txs[skip : skip + count] if all_txs else [] raw_txs = all_txs[skip : skip + count] if all_txs else []
transactions = format_transactions(ci, raw_txs, coin_id) transactions = format_transactions(ci, raw_txs, coin_id)
else:
if coin_id in (Coins.BTC, Coins.LTC):
legacy_funds_info = swap_client.getElectrumLegacyFundsInfo(coin_id)
except Exception as e: except Exception as e:
swap_client.log.warning(f"Failed to fetch transactions for {ticker}: {e}") swap_client.log.warning(f"Failed to fetch transactions for {ticker}: {e}")
@@ -563,6 +567,7 @@ def page_wallet(self, url_split, post_string):
"tx_total": total_transactions, "tx_total": total_transactions,
"tx_limit": tx_filters.get("limit", 30), "tx_limit": tx_filters.get("limit", 30),
"is_electrum_mode": is_electrum_mode, "is_electrum_mode": is_electrum_mode,
"legacy_funds_info": legacy_funds_info,
"use_tor": getattr(swap_client, "use_tor_proxy", False), "use_tor": getattr(swap_client, "use_tor_proxy", False),
}, },
) )