mirror of
https://github.com/basicswap/basicswap.git
synced 2026-05-04 21:42:13 +02:00
Electrum connection stability, swap fixes / UX improvements + Various fixes.
This commit is contained in:
+330
-91
@@ -229,23 +229,35 @@ def checkAndNotifyBalanceChange(
|
||||
cc["cached_balance"] = current_balance
|
||||
cc["cached_total_balance"] = current_total_balance
|
||||
cc["cached_unconfirmed"] = current_unconfirmed
|
||||
swap_client.log.debug(
|
||||
f"{ci.ticker()} balance updated (trigger: {trigger_source})"
|
||||
)
|
||||
balance_event = {
|
||||
"event": "coin_balance_updated",
|
||||
"coin": ci.ticker(),
|
||||
"height": new_height,
|
||||
"trigger": trigger_source,
|
||||
}
|
||||
swap_client.ws_server.send_message_to_all(json.dumps(balance_event))
|
||||
|
||||
suppress = False
|
||||
if cached_balance is None or cached_total_balance is None:
|
||||
suppress = True
|
||||
elif hasattr(ci, "getBackend") and ci.useBackend():
|
||||
backend = ci.getBackend()
|
||||
if backend and hasattr(backend, "recentlyReconnected"):
|
||||
if backend.recentlyReconnected(grace_seconds=30):
|
||||
suppress = True
|
||||
|
||||
if suppress:
|
||||
swap_client.log.debug(
|
||||
f"{ci.ticker()} balance cache updated silently (trigger: {trigger_source})"
|
||||
)
|
||||
else:
|
||||
swap_client.log.debug(
|
||||
f"{ci.ticker()} balance updated (trigger: {trigger_source})"
|
||||
)
|
||||
balance_event = {
|
||||
"event": "coin_balance_updated",
|
||||
"coin": ci.ticker(),
|
||||
"height": new_height,
|
||||
"trigger": trigger_source,
|
||||
}
|
||||
swap_client.ws_server.send_message_to_all(json.dumps(balance_event))
|
||||
except Exception as e:
|
||||
swap_client.log.debug(
|
||||
f"checkAndNotifyBalanceChange {ci.ticker()}: balance check failed: {e}"
|
||||
)
|
||||
cc["cached_balance"] = None
|
||||
cc["cached_total_balance"] = None
|
||||
cc["cached_unconfirmed"] = None
|
||||
|
||||
|
||||
def threadPollXMRChainState(swap_client, coin_type):
|
||||
@@ -458,6 +470,7 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
|
||||
self._updating_wallets_info = {}
|
||||
self._last_updated_wallets_info = 0
|
||||
self._synced_addresses_from_full_node = set()
|
||||
self._cached_electrum_legacy_funds = {}
|
||||
|
||||
self.check_updates_seconds = self.get_int_setting(
|
||||
"check_updates_seconds", 24 * 60 * 60, 60 * 60, 7 * 24 * 60 * 60
|
||||
@@ -581,6 +594,7 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
|
||||
self.thread_pool = concurrent.futures.ThreadPoolExecutor(
|
||||
max_workers=4, thread_name_prefix="bsp"
|
||||
)
|
||||
self._electrum_spend_check_futures = {}
|
||||
|
||||
# Encode key to match network
|
||||
wif_prefix = chainparams[Coins.PART][self.chain]["key_prefix"]
|
||||
@@ -736,24 +750,32 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
|
||||
self.delay_event.set()
|
||||
self.chainstate_delay_event.set()
|
||||
|
||||
if self._network:
|
||||
self._network.stopNetwork()
|
||||
self._network = None
|
||||
|
||||
for coin_type, interface in self.coin_interfaces.items():
|
||||
if hasattr(interface, "_backend") and interface._backend is not None:
|
||||
for coin_type, cc in self.coin_clients.items():
|
||||
interface = cc.get("interface")
|
||||
if (
|
||||
interface
|
||||
and hasattr(interface, "_backend")
|
||||
and interface._backend is not None
|
||||
):
|
||||
try:
|
||||
if hasattr(interface._backend, "_server"):
|
||||
interface._backend._server.disconnect()
|
||||
if hasattr(interface._backend._server, "shutdown"):
|
||||
interface._backend._server.shutdown()
|
||||
else:
|
||||
interface._backend._server.disconnect()
|
||||
self.log.debug(f"Disconnected electrum backend for {coin_type}")
|
||||
except Exception as e:
|
||||
self.log.debug(f"Error disconnecting electrum backend: {e}")
|
||||
|
||||
if self._network:
|
||||
self._network.stopNetwork()
|
||||
self._network = None
|
||||
|
||||
self.log.info("Stopping threads.")
|
||||
for t in self.threads:
|
||||
if hasattr(t, "stop") and callable(t.stop):
|
||||
t.stop()
|
||||
t.join()
|
||||
t.join(timeout=15)
|
||||
|
||||
if sys.version_info[1] >= 9:
|
||||
self.thread_pool.shutdown(cancel_futures=True)
|
||||
@@ -1927,15 +1949,29 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
|
||||
cc["cached_total_balance"] = current_total
|
||||
cc["cached_unconfirmed"] = current_total - current_balance
|
||||
|
||||
balance_event = {
|
||||
"event": "coin_balance_updated",
|
||||
"coin": ci.ticker(),
|
||||
"height": cc.get("chain_height", 0),
|
||||
"trigger": "electrum_notification",
|
||||
"address": address[:20] + "..." if address else None,
|
||||
}
|
||||
self.ws_server.send_message_to_all(json.dumps(balance_event))
|
||||
self.log.debug(f"Electrum notification: {ci.ticker()} balance updated")
|
||||
suppress = False
|
||||
if hasattr(ci, "getBackend") and ci.useBackend():
|
||||
backend = ci.getBackend()
|
||||
if backend and hasattr(backend, "recentlyReconnected"):
|
||||
if backend.recentlyReconnected(grace_seconds=30):
|
||||
suppress = True
|
||||
|
||||
if suppress:
|
||||
self.log.debug(
|
||||
f"Electrum notification: {ci.ticker()} balance cache updated silently (recent reconnection)"
|
||||
)
|
||||
else:
|
||||
balance_event = {
|
||||
"event": "coin_balance_updated",
|
||||
"coin": ci.ticker(),
|
||||
"height": cc.get("chain_height", 0),
|
||||
"trigger": "electrum_notification",
|
||||
"address": address[:20] + "..." if address else None,
|
||||
}
|
||||
self.ws_server.send_message_to_all(json.dumps(balance_event))
|
||||
self.log.debug(
|
||||
f"Electrum notification: {ci.ticker()} balance updated"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
self.log.debug(f"Error handling electrum notification: {e}")
|
||||
@@ -2178,7 +2214,8 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
|
||||
f"will sync keypool and trigger rescan in full node"
|
||||
)
|
||||
return {
|
||||
"empty": True,
|
||||
"empty": False,
|
||||
"reason": "has_balance",
|
||||
"has_balance": True,
|
||||
"balance_sats": balance_sats,
|
||||
"message": (
|
||||
@@ -2495,9 +2532,7 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
|
||||
else:
|
||||
self.log.warning(f"Sweep skipped for {coin_name}: {reason}")
|
||||
elif result.get("txid"):
|
||||
self.log.info(
|
||||
f"Sweep completed: {result.get('amount', 0) / 1e8:.8f} {coin_name} swept to RPC wallet"
|
||||
)
|
||||
pass
|
||||
elif result.get("error"):
|
||||
self.log.warning(
|
||||
f"Sweep failed for {coin_name}: {result.get('error')}"
|
||||
@@ -2745,6 +2780,7 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
|
||||
{
|
||||
"coin_type": int(coin_type),
|
||||
"coin_name": coin_name,
|
||||
"ticker": chainparams[coin_type]["ticker"],
|
||||
"amount": send_amount / 1e8,
|
||||
"fee": fee / 1e8,
|
||||
"txid": txid_hex,
|
||||
@@ -2823,15 +2859,27 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
|
||||
except Exception as e:
|
||||
return {"error": f"Failed to list UTXOs: {e}"}
|
||||
|
||||
hrp = ci.chainparams_network().get("hrp", "bc")
|
||||
bip84_addresses = set()
|
||||
wm = ci.getWalletManager()
|
||||
if wm:
|
||||
try:
|
||||
all_addrs = wm.getAllAddresses(coin_type, include_watch_only=False)
|
||||
bip84_addresses = set(all_addrs)
|
||||
except Exception as e:
|
||||
self.log.debug(f"Error getting BIP84 addresses: {e}")
|
||||
|
||||
legacy_utxos = []
|
||||
total_legacy_sats = 0
|
||||
|
||||
for u in unspent:
|
||||
if "address" not in u:
|
||||
if "address" not in u or "txid" not in u:
|
||||
continue
|
||||
if "vout" not in u and "n" not in u:
|
||||
continue
|
||||
addr = u["address"]
|
||||
if not addr.startswith(hrp + "1"):
|
||||
if addr not in bip84_addresses:
|
||||
if "vout" not in u and "n" in u:
|
||||
u["vout"] = u["n"]
|
||||
legacy_utxos.append(u)
|
||||
total_legacy_sats += ci.make_int(u.get("amount", 0))
|
||||
|
||||
@@ -2855,10 +2903,26 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
|
||||
"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}"}
|
||||
new_address = None
|
||||
if wm:
|
||||
try:
|
||||
new_address = wm.getNewAddress(coin_type, internal=False)
|
||||
self.log.info(
|
||||
f"[Consolidate {coin_name}] Using BIP84 address: {new_address}"
|
||||
)
|
||||
except Exception as e:
|
||||
self.log.warning(f"Failed to get BIP84 address: {e}")
|
||||
|
||||
if not new_address:
|
||||
try:
|
||||
new_address = ci.rpc_wallet(
|
||||
"getnewaddress", ["consolidate", "bech32"]
|
||||
)
|
||||
self.log.warning(
|
||||
f"[Consolidate {coin_name}] Using Core address (not BIP84): {new_address}"
|
||||
)
|
||||
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)
|
||||
@@ -2994,6 +3058,12 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
|
||||
return None
|
||||
|
||||
def getElectrumLegacyFundsInfo(self, coin_type: Coins) -> dict:
|
||||
cached = self._cached_electrum_legacy_funds.get(int(coin_type))
|
||||
if cached is not None:
|
||||
return cached
|
||||
return self._computeElectrumLegacyFundsInfo(coin_type)
|
||||
|
||||
def _computeElectrumLegacyFundsInfo(self, coin_type: Coins) -> dict:
|
||||
try:
|
||||
cc = self.coin_clients.get(coin_type)
|
||||
if not cc or cc.get("connection_type") != "electrum":
|
||||
@@ -3027,7 +3097,7 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
|
||||
}
|
||||
return {"has_legacy_funds": False}
|
||||
except Exception as e:
|
||||
self.log.debug(f"getElectrumLegacyFundsInfo error: {e}")
|
||||
self.log.debug(f"_computeElectrumLegacyFundsInfo error: {e}")
|
||||
return {"has_legacy_funds": False}
|
||||
|
||||
def _tryGetFullNodeAddresses(self, coin_type: Coins) -> list:
|
||||
@@ -4528,10 +4598,12 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
|
||||
self.log.info_s(f"In txn: {txid}")
|
||||
return txid
|
||||
|
||||
def withdrawLTC(self, type_from, value, addr_to, subfee: bool) -> str:
|
||||
ci = self.ci(Coins.LTC)
|
||||
def withdrawCoinExtended(
|
||||
self, coin_type, type_from, value, addr_to, subfee: bool
|
||||
) -> str:
|
||||
ci = self.ci(coin_type)
|
||||
self.log.info(
|
||||
"withdrawLTC{}".format(
|
||||
"withdrawCoinExtended{}".format(
|
||||
""
|
||||
if self.log.safe_logs
|
||||
else " {} {} to {} {}".format(
|
||||
@@ -7454,6 +7526,7 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
|
||||
dest_address,
|
||||
bid.amount_to,
|
||||
bid.chain_b_height_start,
|
||||
find_index=True,
|
||||
vout=bid.xmr_b_lock_tx.vout,
|
||||
)
|
||||
else:
|
||||
@@ -7498,7 +7571,7 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
|
||||
bid_id=bid.bid_id,
|
||||
tx_type=TxTypes.XMR_SWAP_B_LOCK,
|
||||
txid=xmr_swap.b_lock_tx_id,
|
||||
vout=0,
|
||||
vout=found_tx.get("index", 0),
|
||||
)
|
||||
if bid.xmr_b_lock_tx.txid != found_txid:
|
||||
self.log.debug(
|
||||
@@ -9328,6 +9401,95 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
|
||||
|
||||
c["last_height_checked"] = chain_blocks
|
||||
|
||||
def _fetchSpendsElectrum(self, coin_type, watched_outputs, watched_scripts):
|
||||
ci = self.ci(coin_type)
|
||||
results = {"outputs": [], "scripts": [], "chain_blocks": 0}
|
||||
|
||||
try:
|
||||
results["chain_blocks"] = ci.getChainHeight()
|
||||
except Exception as e:
|
||||
self.log.debug(f"_fetchSpendsElectrum getChainHeight error: {e}")
|
||||
return results
|
||||
|
||||
for o in watched_outputs:
|
||||
if self.delay_event.is_set():
|
||||
return results
|
||||
try:
|
||||
spend_info = ci.checkWatchedOutput(o.txid_hex, o.vout)
|
||||
if spend_info:
|
||||
raw_tx = ci.getBackend().getTransactionRaw(spend_info["txid"])
|
||||
if raw_tx:
|
||||
tx = ci.loadTx(bytes.fromhex(raw_tx))
|
||||
vin_list = []
|
||||
for idx, inp in enumerate(tx.vin):
|
||||
vin_entry = {
|
||||
"txid": f"{inp.prevout.hash:064x}",
|
||||
"vout": inp.prevout.n,
|
||||
}
|
||||
if tx.wit and idx < len(tx.wit.vtxinwit):
|
||||
wit = tx.wit.vtxinwit[idx]
|
||||
if wit.scriptWitness and wit.scriptWitness.stack:
|
||||
vin_entry["txinwitness"] = [
|
||||
item.hex() for item in wit.scriptWitness.stack
|
||||
]
|
||||
vin_list.append(vin_entry)
|
||||
tx_dict = {
|
||||
"txid": spend_info["txid"],
|
||||
"hex": raw_tx,
|
||||
"vin": vin_list,
|
||||
"vout": [
|
||||
{
|
||||
"value": ci.format_amount(out.nValue),
|
||||
"n": i,
|
||||
"scriptPubKey": {"hex": out.scriptPubKey.hex()},
|
||||
}
|
||||
for i, out in enumerate(tx.vout)
|
||||
],
|
||||
}
|
||||
results["outputs"].append((o, spend_info, tx_dict))
|
||||
except Exception as e:
|
||||
self.log.debug(f"_fetchSpendsElectrum checkWatchedOutput error: {e}")
|
||||
|
||||
for s in watched_scripts:
|
||||
if self.delay_event.is_set():
|
||||
return results
|
||||
try:
|
||||
found = ci.checkWatchedScript(s.script)
|
||||
if found:
|
||||
results["scripts"].append((s, found))
|
||||
except Exception as e:
|
||||
self.log.debug(f"_fetchSpendsElectrum checkWatchedScript error: {e}")
|
||||
|
||||
return results
|
||||
|
||||
def _processFetchedSpends(self, coin_type, results):
|
||||
c = self.coin_clients[coin_type]
|
||||
|
||||
for o, spend_info, tx_dict in results["outputs"]:
|
||||
try:
|
||||
self.log.debug(
|
||||
f"Found spend via Electrum {self.logIDT(o.txid_hex)} {o.vout} in {self.logIDT(spend_info['txid'])} {spend_info['vin']}"
|
||||
)
|
||||
self.processSpentOutput(
|
||||
coin_type, o, spend_info["txid"], spend_info["vin"], tx_dict
|
||||
)
|
||||
except Exception as e:
|
||||
self.log.debug(f"_processFetchedSpends output error: {e}")
|
||||
|
||||
for s, found in results["scripts"]:
|
||||
try:
|
||||
txid_bytes = bytes.fromhex(found["txid"])
|
||||
self.log.debug(
|
||||
f"Found script via Electrum for bid {self.log.id(s.bid_id)}: {self.logIDT(txid_bytes)} {found['vout']}."
|
||||
)
|
||||
self.processFoundScript(coin_type, s, txid_bytes, found["vout"])
|
||||
except Exception as e:
|
||||
self.log.debug(f"_processFetchedSpends script error: {e}")
|
||||
|
||||
chain_blocks = results.get("chain_blocks", 0)
|
||||
if chain_blocks > 0:
|
||||
c["last_height_checked"] = chain_blocks
|
||||
|
||||
def expireMessageRoutes(self) -> None:
|
||||
if self._is_locked is True:
|
||||
self.log.debug("Not expiring message routes while system is locked")
|
||||
@@ -9526,11 +9688,12 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
|
||||
try:
|
||||
cursor = self.openDB()
|
||||
|
||||
query = "SELECT action_type, linked_id FROM actions WHERE active_ind = 1 AND trigger_at <= :now"
|
||||
query = "SELECT action_id, action_type, linked_id FROM actions WHERE active_ind = 1 AND trigger_at <= :now"
|
||||
rows = cursor.execute(query, {"now": now}).fetchall()
|
||||
retry_action_ids = []
|
||||
|
||||
for row in rows:
|
||||
action_type, linked_id = row
|
||||
action_id, action_type, linked_id = row
|
||||
accepting_bid: bool = False
|
||||
try:
|
||||
if action_type == ActionTypes.ACCEPT_BID:
|
||||
@@ -9562,6 +9725,11 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
|
||||
self.acceptADSReverseBid(linked_id, cursor)
|
||||
else:
|
||||
self.log.warning(f"Unknown event type: {action_type}")
|
||||
except TemporaryError as ex:
|
||||
self.log.warning(
|
||||
f"checkQueuedActions temporary error for {self.log.id(linked_id)}: {ex}"
|
||||
)
|
||||
retry_action_ids.append(action_id)
|
||||
except Exception as ex:
|
||||
err_msg = f"checkQueuedActions failed: {ex}"
|
||||
self.logException(err_msg)
|
||||
@@ -9594,10 +9762,23 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
|
||||
bid.setState(BidStates.BID_ERROR)
|
||||
self.saveBidInSession(bid_id, bid, cursor)
|
||||
|
||||
query: str = "DELETE FROM actions WHERE trigger_at <= :now"
|
||||
if self.debug:
|
||||
query = "UPDATE actions SET active_ind = 2 WHERE trigger_at <= :now"
|
||||
cursor.execute(query, {"now": now})
|
||||
if retry_action_ids:
|
||||
placeholders = ",".join(
|
||||
f":retry_{i}" for i in range(len(retry_action_ids))
|
||||
)
|
||||
params = {"now": now}
|
||||
for i, aid in enumerate(retry_action_ids):
|
||||
params[f"retry_{i}"] = aid
|
||||
if self.debug:
|
||||
query = f"UPDATE actions SET active_ind = 2 WHERE trigger_at <= :now AND action_id NOT IN ({placeholders})"
|
||||
else:
|
||||
query = f"DELETE FROM actions WHERE trigger_at <= :now AND action_id NOT IN ({placeholders})"
|
||||
cursor.execute(query, params)
|
||||
else:
|
||||
query: str = "DELETE FROM actions WHERE trigger_at <= :now"
|
||||
if self.debug:
|
||||
query = "UPDATE actions SET active_ind = 2 WHERE trigger_at <= :now"
|
||||
cursor.execute(query, {"now": now})
|
||||
|
||||
except Exception as ex:
|
||||
self.handleSessionErrors(ex, cursor, "checkQueuedActions")
|
||||
@@ -11349,13 +11530,18 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
|
||||
)
|
||||
|
||||
try:
|
||||
b_lock_tx_id = ci_to.publishBLockTx(
|
||||
b_lock_vout = 0
|
||||
result = ci_to.publishBLockTx(
|
||||
xmr_swap.vkbv,
|
||||
xmr_swap.pkbs,
|
||||
bid.amount_to,
|
||||
b_fee_rate,
|
||||
unlock_time=unlock_time,
|
||||
)
|
||||
if isinstance(result, tuple):
|
||||
b_lock_tx_id, b_lock_vout = result
|
||||
else:
|
||||
b_lock_tx_id = result
|
||||
if bid.debug_ind == DebugTypes.B_LOCK_TX_MISSED_SEND:
|
||||
self.log.debug(
|
||||
f"Adaptor-sig bid {self.log.id(bid_id)}: Debug {bid.debug_ind} - Losing XMR lock tx {self.log.id(b_lock_tx_id)}."
|
||||
@@ -11413,7 +11599,7 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
|
||||
bid_id=bid_id,
|
||||
tx_type=TxTypes.XMR_SWAP_B_LOCK,
|
||||
txid=b_lock_tx_id,
|
||||
vout=0,
|
||||
vout=b_lock_vout,
|
||||
)
|
||||
xmr_swap.b_lock_tx_id = b_lock_tx_id
|
||||
bid.xmr_b_lock_tx.setState(TxStates.TX_SENT)
|
||||
@@ -12818,18 +13004,30 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
|
||||
if self._zmq_queue_enabled and self.zmqSubscriber:
|
||||
try:
|
||||
if self._read_zmq_queue:
|
||||
topic, message, seq = self.zmqSubscriber.recv_multipart(
|
||||
flags=zmq.NOBLOCK
|
||||
)
|
||||
if topic == b"smsg":
|
||||
self.processZmqSmsg(message)
|
||||
elif topic == b"hashwtx":
|
||||
self.processZmqHashwtx(message)
|
||||
for _i in range(100):
|
||||
topic, message, seq = self.zmqSubscriber.recv_multipart(
|
||||
flags=zmq.NOBLOCK
|
||||
)
|
||||
if topic == b"smsg":
|
||||
self.processZmqSmsg(message)
|
||||
elif topic == b"hashwtx":
|
||||
self.processZmqHashwtx(message)
|
||||
except zmq.Again as e: # noqa: F841
|
||||
pass
|
||||
except Exception as e:
|
||||
self.logException(f"smsg zmq {e}")
|
||||
|
||||
for k, future in list(self._electrum_spend_check_futures.items()):
|
||||
if future.done():
|
||||
try:
|
||||
results = future.result()
|
||||
self._processFetchedSpends(k, results)
|
||||
except Exception as e:
|
||||
self.log.debug(
|
||||
f"Background electrum spend check error for {Coins(k).name}: {e}"
|
||||
)
|
||||
del self._electrum_spend_check_futures[k]
|
||||
|
||||
self.updateNetwork()
|
||||
|
||||
try:
|
||||
@@ -12891,7 +13089,21 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
|
||||
):
|
||||
continue
|
||||
if len(c["watched_outputs"]) > 0 or len(c["watched_scripts"]):
|
||||
self.checkForSpends(k, c)
|
||||
if c.get("connection_type") == "electrum":
|
||||
if (
|
||||
k not in self._electrum_spend_check_futures
|
||||
or self._electrum_spend_check_futures[k].done()
|
||||
):
|
||||
self._electrum_spend_check_futures[k] = (
|
||||
self.thread_pool.submit(
|
||||
self._fetchSpendsElectrum,
|
||||
k,
|
||||
list(c["watched_outputs"]),
|
||||
list(c["watched_scripts"]),
|
||||
)
|
||||
)
|
||||
else:
|
||||
self.checkForSpends(k, c)
|
||||
self._last_checked_watched = now
|
||||
|
||||
if now - self._last_checked_expired >= self.check_expired_seconds:
|
||||
@@ -13250,39 +13462,35 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
|
||||
display_name = getCoinName(coin_id)
|
||||
|
||||
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)
|
||||
if migration_result.get("success"):
|
||||
count = migration_result.get("count", 0)
|
||||
self.log.info(
|
||||
f"Lite wallet ready for {coin_name} with {count} addresses"
|
||||
)
|
||||
if migration_message:
|
||||
migration_message += (
|
||||
f" Lite wallet ready ({count} addresses)."
|
||||
migration_message = (
|
||||
f"Lite wallet ready for {display_name} ({count} addresses)."
|
||||
)
|
||||
|
||||
auto_transfer_now = data.get("auto_transfer_now", False)
|
||||
if auto_transfer_now:
|
||||
transfer_result = self._consolidateLegacyFundsToSegwit(
|
||||
coin_id
|
||||
)
|
||||
else:
|
||||
migration_message = f"Lite wallet ready for {display_name} ({count} addresses)."
|
||||
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')}"
|
||||
)
|
||||
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')}"
|
||||
)
|
||||
else:
|
||||
error = migration_result.get("error", "unknown")
|
||||
reason = migration_result.get("reason", "")
|
||||
@@ -13333,14 +13541,23 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
|
||||
f"Transfer failed for {coin_name}: {error}"
|
||||
)
|
||||
raise ValueError(f"Transfer failed: {error}")
|
||||
elif reason in ("has_balance", "active_swap"):
|
||||
elif reason == "active_swap":
|
||||
error = empty_check.get(
|
||||
"message", "Wallet must be empty before switching modes"
|
||||
"message", "Cannot switch: active swap in progress"
|
||||
)
|
||||
self.log.error(
|
||||
f"Migration blocked for {coin_name}: {error}"
|
||||
)
|
||||
raise ValueError(error)
|
||||
elif reason == "has_balance":
|
||||
balance_msg = empty_check.get("message", "")
|
||||
self.log.warning(
|
||||
f"Switching {coin_name} to RPC without transfer: {balance_msg}"
|
||||
)
|
||||
migration_message = (
|
||||
f"{display_name} has funds on lite wallet addresses. "
|
||||
f"Keypool will be synced and rescan triggered."
|
||||
)
|
||||
|
||||
sync_result = self._syncWalletIndicesToRPC(coin_id)
|
||||
if sync_result.get("success"):
|
||||
@@ -13712,6 +13929,26 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
|
||||
rv["mweb_pending"] = walletinfo.get(
|
||||
"mweb_unconfirmed", 0
|
||||
) + walletinfo.get("mweb_immature", 0)
|
||||
elif coin == Coins.FIRO:
|
||||
try:
|
||||
rv["spark_address"] = self.getCachedStealthAddressForCoin(
|
||||
Coins.FIRO
|
||||
)
|
||||
except Exception as e:
|
||||
self.log.warning(
|
||||
f"getCachedStealthAddressForCoin for {ci.coin_name()} failed with: {e}."
|
||||
)
|
||||
rv["spark_balance"] = (
|
||||
0
|
||||
if walletinfo["spark_balance"] == 0
|
||||
else ci.format_amount(walletinfo["spark_balance"])
|
||||
)
|
||||
spark_pending_int = (
|
||||
walletinfo["spark_unconfirmed"] + walletinfo["spark_immature"]
|
||||
)
|
||||
rv["spark_pending"] = (
|
||||
0 if spark_pending_int == 0 else ci.format_amount(spark_pending_int)
|
||||
)
|
||||
|
||||
return rv
|
||||
except Exception as e:
|
||||
@@ -13719,9 +13956,9 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
|
||||
|
||||
def addWalletInfoRecord(self, coin, info_type, wi) -> None:
|
||||
coin_id = int(coin)
|
||||
now: int = self.getTime()
|
||||
cursor = self.openDB()
|
||||
try:
|
||||
now: int = self.getTime()
|
||||
self.add(
|
||||
Wallets(
|
||||
coin_id=coin,
|
||||
@@ -13975,6 +14212,8 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
|
||||
if row2[0].startswith("stealth"):
|
||||
if coin_id == Coins.LTC:
|
||||
wallet_data["mweb_address"] = row2[1]
|
||||
elif coin_id == Coins.FIRO:
|
||||
wallet_data["spark_address"] = row2[1]
|
||||
else:
|
||||
wallet_data["stealth_address"] = row2[1]
|
||||
else:
|
||||
|
||||
+22
-17
@@ -36,22 +36,25 @@ def signal_handler(sig, frame):
|
||||
os.write(
|
||||
sys.stdout.fileno(), f"Signal {sig} detected, ending program.\n".encode("utf-8")
|
||||
)
|
||||
if swap_client is not None and not swap_client.chainstate_delay_event.is_set():
|
||||
try:
|
||||
from basicswap.ui.page_amm import stop_amm_process, get_amm_status
|
||||
try:
|
||||
if swap_client is not None and not swap_client.chainstate_delay_event.is_set():
|
||||
try:
|
||||
from basicswap.ui.page_amm import stop_amm_process, get_amm_status
|
||||
|
||||
amm_status = get_amm_status()
|
||||
if amm_status == "running":
|
||||
logger.info("Signal handler stopping AMM process...")
|
||||
success, msg = stop_amm_process(swap_client)
|
||||
if success:
|
||||
logger.info(f"AMM signal shutdown: {msg}")
|
||||
else:
|
||||
logger.warning(f"AMM signal shutdown warning: {msg}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error stopping AMM in signal handler: {e}")
|
||||
amm_status = get_amm_status()
|
||||
if amm_status == "running":
|
||||
logger.info("Signal handler stopping AMM process...")
|
||||
success, msg = stop_amm_process(swap_client)
|
||||
if success:
|
||||
logger.info(f"AMM signal shutdown: {msg}")
|
||||
else:
|
||||
logger.warning(f"AMM signal shutdown warning: {msg}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error stopping AMM in signal handler: {e}")
|
||||
|
||||
swap_client.stopRunning()
|
||||
swap_client.stopRunning()
|
||||
except NameError:
|
||||
pass
|
||||
|
||||
|
||||
def checkPARTZmqConfigBeforeStart(part_settings, swap_settings):
|
||||
@@ -618,7 +621,7 @@ def runClient(
|
||||
signal.CTRL_C_EVENT if os.name == "nt" else signal.SIGINT
|
||||
)
|
||||
except Exception as e:
|
||||
swap_client.log.info(f"Interrupting {d.name} {d.handle.pid}, error {e}")
|
||||
swap_client.log.error(f"Interrupting {d.name} {d.handle.pid}: {e}")
|
||||
for d in daemons:
|
||||
try:
|
||||
d.handle.wait(timeout=120)
|
||||
@@ -627,10 +630,12 @@ def runClient(
|
||||
fp.close()
|
||||
closed_pids.append(d.handle.pid)
|
||||
except Exception as e:
|
||||
swap_client.log.error(f"Error: {e}")
|
||||
swap_client.log.error(
|
||||
f"Waiting for {d.name} {d.handle.pid} to shutdown: {e}"
|
||||
)
|
||||
|
||||
fail_code: int = swap_client.fail_code
|
||||
del swap_client
|
||||
swap_client = None
|
||||
|
||||
if os.path.exists(pids_path):
|
||||
with open(pids_path) as fd:
|
||||
|
||||
@@ -6,8 +6,10 @@
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
import os
|
||||
import gzip
|
||||
import json
|
||||
import shlex
|
||||
import hashlib
|
||||
import secrets
|
||||
import traceback
|
||||
import threading
|
||||
@@ -19,6 +21,7 @@ from jinja2 import Environment, PackageLoader
|
||||
from socket import error as SocketError
|
||||
from urllib import parse
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from email.utils import formatdate, parsedate_to_datetime
|
||||
from http.cookies import SimpleCookie
|
||||
|
||||
from . import __version__
|
||||
@@ -802,7 +805,6 @@ class HttpHandler(BaseHTTPRequestHandler):
|
||||
if page == "static":
|
||||
try:
|
||||
static_path = os.path.join(os.path.dirname(__file__), "static")
|
||||
content = None
|
||||
mime_type = ""
|
||||
filepath = ""
|
||||
if len(url_split) > 3 and url_split[2] == "sequence_diagrams":
|
||||
@@ -835,9 +837,73 @@ class HttpHandler(BaseHTTPRequestHandler):
|
||||
if mime_type == "" or not filepath:
|
||||
raise ValueError("Unknown file type or path")
|
||||
|
||||
file_stat = os.stat(filepath)
|
||||
mtime = file_stat.st_mtime
|
||||
file_size = file_stat.st_size
|
||||
|
||||
etag_hash = hashlib.md5(
|
||||
f"{file_size}-{mtime}".encode()
|
||||
).hexdigest()
|
||||
etag = f'"{etag_hash}"'
|
||||
last_modified = formatdate(mtime, usegmt=True)
|
||||
|
||||
if_none_match = self.headers.get("If-None-Match")
|
||||
if if_none_match:
|
||||
if if_none_match.strip() == "*" or etag in [
|
||||
t.strip() for t in if_none_match.split(",")
|
||||
]:
|
||||
self.send_response(304)
|
||||
self.send_header("ETag", etag)
|
||||
self.send_header("Cache-Control", "public")
|
||||
self.end_headers()
|
||||
return b""
|
||||
|
||||
if_modified_since = self.headers.get("If-Modified-Since")
|
||||
if if_modified_since and not if_none_match:
|
||||
try:
|
||||
ims_time = parsedate_to_datetime(if_modified_since)
|
||||
file_time = datetime.fromtimestamp(int(mtime), tz=timezone.utc)
|
||||
if file_time <= ims_time:
|
||||
self.send_response(304)
|
||||
self.send_header("Last-Modified", last_modified)
|
||||
self.send_header("Cache-Control", "public")
|
||||
self.end_headers()
|
||||
return b""
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
|
||||
is_lib = len(url_split) > 4 and url_split[3] == "libs"
|
||||
if is_lib:
|
||||
cache_control = "public, max-age=31536000, immutable"
|
||||
elif url_split[2] in ("css", "js"):
|
||||
cache_control = "public, max-age=3600, must-revalidate"
|
||||
elif url_split[2] in ("images", "sequence_diagrams"):
|
||||
cache_control = "public, max-age=86400"
|
||||
else:
|
||||
cache_control = "public, max-age=3600"
|
||||
|
||||
with open(filepath, "rb") as fp:
|
||||
content = fp.read()
|
||||
self.putHeaders(status_code, mime_type)
|
||||
|
||||
extra_headers = [
|
||||
("Cache-Control", cache_control),
|
||||
("Last-Modified", last_modified),
|
||||
("ETag", etag),
|
||||
]
|
||||
|
||||
is_compressible = mime_type in (
|
||||
"text/css; charset=utf-8",
|
||||
"application/javascript",
|
||||
"image/svg+xml",
|
||||
)
|
||||
accept_encoding = self.headers.get("Accept-Encoding", "")
|
||||
if is_compressible and "gzip" in accept_encoding:
|
||||
content = gzip.compress(content)
|
||||
extra_headers.append(("Content-Encoding", "gzip"))
|
||||
extra_headers.append(("Vary", "Accept-Encoding"))
|
||||
|
||||
extra_headers.append(("Content-Length", str(len(content))))
|
||||
self.putHeaders(status_code, mime_type, extra_headers=extra_headers)
|
||||
return content
|
||||
|
||||
except FileNotFoundError:
|
||||
|
||||
+140
-65
@@ -15,6 +15,7 @@ import os
|
||||
import shutil
|
||||
import sqlite3
|
||||
import threading
|
||||
import time
|
||||
import traceback
|
||||
|
||||
from io import BytesIO
|
||||
@@ -185,6 +186,7 @@ def extractScriptLockRefundScriptValues(script_bytes: bytes):
|
||||
|
||||
class BTCInterface(Secp256k1Interface):
|
||||
_scantxoutset_lock = threading.Lock()
|
||||
_MAX_SCANTXOUTSET_RETRIES = 3
|
||||
|
||||
@staticmethod
|
||||
def coin_type():
|
||||
@@ -233,6 +235,10 @@ class BTCInterface(Secp256k1Interface):
|
||||
def xmr_swap_b_lock_spend_tx_vsize() -> int:
|
||||
return 110
|
||||
|
||||
@staticmethod
|
||||
def getdustlimit() -> int:
|
||||
return 5460
|
||||
|
||||
@staticmethod
|
||||
def txoType():
|
||||
return CTxOut
|
||||
@@ -759,6 +765,19 @@ class BTCInterface(Secp256k1Interface):
|
||||
wm.syncBalances(
|
||||
self.coin_type(), self._backend, funded_only=not do_full_scan
|
||||
)
|
||||
|
||||
try:
|
||||
self._backend.estimateFee(self._conf_target)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
coin_type = self.coin_type()
|
||||
if coin_type in (Coins.BTC, Coins.LTC):
|
||||
result = self._sc._computeElectrumLegacyFundsInfo(coin_type)
|
||||
self._sc._cached_electrum_legacy_funds[int(coin_type)] = result
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
if hasattr(self._backend, "setBackgroundMode"):
|
||||
self._backend.setBackgroundMode(False)
|
||||
@@ -1890,8 +1909,9 @@ class BTCInterface(Secp256k1Interface):
|
||||
rough_vsize = 10 + (len(funded_tx.vout) + 1) * 34 + len(selected_utxos) * 68
|
||||
rough_fee = max(round(feerate_satkb * rough_vsize / 1000), min_relay_fee)
|
||||
rough_change = total_input - total_output - rough_fee
|
||||
dust_limit = self.getdustlimit()
|
||||
|
||||
if rough_change > 1000:
|
||||
if rough_change > dust_limit:
|
||||
change_addr = wm.getNewInternalAddress(self.coin_type())
|
||||
if not change_addr:
|
||||
change_addr = wm.getExistingInternalAddress(self.coin_type())
|
||||
@@ -1907,7 +1927,7 @@ class BTCInterface(Secp256k1Interface):
|
||||
final_fee = max(round(feerate_satkb * final_vsize / 1000), min_relay_fee)
|
||||
change = total_input - total_output - final_fee
|
||||
|
||||
if change > 1000:
|
||||
if change > dust_limit:
|
||||
funded_tx.vout[-1].nValue = change
|
||||
else:
|
||||
funded_tx.vout.pop()
|
||||
@@ -2418,10 +2438,37 @@ class BTCInterface(Secp256k1Interface):
|
||||
def getPkDest(self, K: bytes) -> bytearray:
|
||||
return self.getScriptForPubkeyHash(self.getPubkeyHash(K))
|
||||
|
||||
def _rpc_scantxoutset(self, descriptors: list):
|
||||
with BTCInterface._scantxoutset_lock:
|
||||
for attempt in range(self._MAX_SCANTXOUTSET_RETRIES):
|
||||
try:
|
||||
return self.rpc("scantxoutset", ["start", descriptors])
|
||||
except ValueError as e:
|
||||
if "Scan already in progress" in str(e):
|
||||
self._log.warning(
|
||||
"scantxoutset: scan already in progress (attempt %d/%d), aborting",
|
||||
attempt + 1,
|
||||
self._MAX_SCANTXOUTSET_RETRIES,
|
||||
)
|
||||
try:
|
||||
self.rpc("scantxoutset", ["abort"])
|
||||
except Exception as abort_err:
|
||||
self._log.debug(
|
||||
"scantxoutset abort returned: %s", abort_err
|
||||
)
|
||||
time.sleep(0.5)
|
||||
else:
|
||||
raise
|
||||
raise ValueError(
|
||||
"scantxoutset failed after {} retries – scan could not be started".format(
|
||||
self._MAX_SCANTXOUTSET_RETRIES
|
||||
)
|
||||
)
|
||||
|
||||
def scanTxOutset(self, dest):
|
||||
if self._connection_type == "electrum":
|
||||
return self._scanTxOutsetElectrum(dest)
|
||||
return self.rpc("scantxoutset", ["start", ["raw({})".format(dest.hex())]])
|
||||
return self._rpc_scantxoutset(["raw({})".format(dest.hex())])
|
||||
|
||||
def _scanTxOutsetElectrum(self, dest):
|
||||
backend = self.getBackend()
|
||||
@@ -2572,15 +2619,19 @@ class BTCInterface(Secp256k1Interface):
|
||||
def encodeSharedAddress(self, Kbv, Kbs):
|
||||
return self.pubkey_to_segwit_address(Kbs)
|
||||
|
||||
def publishBLockTx(
|
||||
self, kbv, Kbs, output_amount, feerate, unlock_time: int = 0
|
||||
) -> bytes:
|
||||
def publishBLockTx(self, kbv, Kbs, output_amount, feerate, unlock_time: int = 0):
|
||||
b_lock_tx = self.createBLockTx(Kbs, output_amount)
|
||||
|
||||
b_lock_tx = self.fundTx(b_lock_tx, feerate)
|
||||
|
||||
script_pk = self.getPkDest(Kbs)
|
||||
funded_tx = self.loadTx(b_lock_tx)
|
||||
lock_vout = findOutput(funded_tx, script_pk)
|
||||
|
||||
b_lock_tx = self.signTxWithWallet(b_lock_tx)
|
||||
|
||||
return bytes.fromhex(self.publishTx(b_lock_tx))
|
||||
txid = bytes.fromhex(self.publishTx(b_lock_tx))
|
||||
return txid, lock_vout
|
||||
|
||||
def getTxVSize(self, tx, add_bytes: int = 0, add_witness_bytes: int = 0) -> int:
|
||||
wsf = self.witnessScaleFactor()
|
||||
@@ -2604,7 +2655,9 @@ class BTCInterface(Secp256k1Interface):
|
||||
if self.using_segwit()
|
||||
else self.pubkey_to_address(Kbs)
|
||||
)
|
||||
return self.getLockTxHeight(None, dest_address, cb_swap_value, restore_height)
|
||||
return self.getLockTxHeight(
|
||||
None, dest_address, cb_swap_value, restore_height, find_index=True
|
||||
)
|
||||
|
||||
"""
|
||||
raw_dest = self.getPkDest(Kbs)
|
||||
@@ -2646,44 +2699,66 @@ class BTCInterface(Secp256k1Interface):
|
||||
self._log.id(chain_b_lock_txid), lock_tx_vout
|
||||
)
|
||||
)
|
||||
locked_n = lock_tx_vout
|
||||
|
||||
Kbs = self.getPubkey(kbs)
|
||||
script_pk = self.getPkDest(Kbs)
|
||||
|
||||
if locked_n is None:
|
||||
if self.useBackend():
|
||||
backend = self.getBackend()
|
||||
tx_hex = backend.getTransactionRaw(chain_b_lock_txid.hex())
|
||||
if tx_hex:
|
||||
lock_tx = self.loadTx(bytes.fromhex(tx_hex))
|
||||
locked_n = findOutput(lock_tx, script_pk)
|
||||
if locked_n is None:
|
||||
self._log.error(
|
||||
f"spendBLockTx: Output not found in tx {chain_b_lock_txid.hex()}, "
|
||||
f"script_pk={script_pk.hex()}, num_outputs={len(lock_tx.vout)}"
|
||||
)
|
||||
for i, out in enumerate(lock_tx.vout):
|
||||
self._log.debug(
|
||||
f" vout[{i}]: value={out.nValue}, scriptPubKey={out.scriptPubKey.hex()}"
|
||||
)
|
||||
else:
|
||||
self._log.warning(
|
||||
f"spendBLockTx: Failed to fetch tx {chain_b_lock_txid.hex()} from electrum, "
|
||||
f"defaulting to vout=0 (standard for B lock transactions)"
|
||||
)
|
||||
locked_n = 0
|
||||
else:
|
||||
wtx = self.rpc_wallet_watch(
|
||||
"gettransaction",
|
||||
[
|
||||
chain_b_lock_txid.hex(),
|
||||
],
|
||||
)
|
||||
lock_tx = self.loadTx(bytes.fromhex(wtx["hex"]))
|
||||
locked_n = None
|
||||
actual_value = None
|
||||
if self.useBackend():
|
||||
backend = self.getBackend()
|
||||
tx_hex = backend.getTransactionRaw(chain_b_lock_txid.hex())
|
||||
if tx_hex:
|
||||
lock_tx = self.loadTx(bytes.fromhex(tx_hex))
|
||||
locked_n = findOutput(lock_tx, script_pk)
|
||||
if locked_n is not None:
|
||||
actual_value = lock_tx.vout[locked_n].nValue
|
||||
else:
|
||||
self._log.error(
|
||||
f"spendBLockTx: Output not found in tx {chain_b_lock_txid.hex()}, "
|
||||
f"script_pk={script_pk.hex()}, num_outputs={len(lock_tx.vout)}"
|
||||
)
|
||||
for i, out in enumerate(lock_tx.vout):
|
||||
self._log.debug(
|
||||
f" vout[{i}]: value={out.nValue}, scriptPubKey={out.scriptPubKey.hex()}"
|
||||
)
|
||||
else:
|
||||
self._log.warning(
|
||||
f"spendBLockTx: Failed to fetch tx {chain_b_lock_txid.hex()} from backend"
|
||||
)
|
||||
locked_n = lock_tx_vout
|
||||
else:
|
||||
wtx = self.rpc_wallet_watch(
|
||||
"gettransaction",
|
||||
[
|
||||
chain_b_lock_txid.hex(),
|
||||
],
|
||||
)
|
||||
lock_tx = self.loadTx(bytes.fromhex(wtx["hex"]))
|
||||
locked_n = findOutput(lock_tx, script_pk)
|
||||
if locked_n is not None:
|
||||
actual_value = lock_tx.vout[locked_n].nValue
|
||||
|
||||
if (
|
||||
locked_n is not None
|
||||
and lock_tx_vout is not None
|
||||
and locked_n != lock_tx_vout
|
||||
):
|
||||
self._log.warning(
|
||||
f"spendBLockTx: Stored vout {lock_tx_vout} differs from actual vout {locked_n} "
|
||||
f"for tx {chain_b_lock_txid.hex()}"
|
||||
)
|
||||
|
||||
ensure(locked_n is not None, "Output not found in tx")
|
||||
|
||||
spend_value = cb_swap_value
|
||||
if spend_actual_balance and actual_value is not None:
|
||||
if actual_value != cb_swap_value:
|
||||
self._log.warning(
|
||||
f"spendBLockTx: Spending actual balance {actual_value}, "
|
||||
f"not expected swap value {cb_swap_value}."
|
||||
)
|
||||
spend_value = actual_value
|
||||
|
||||
pkh_to = self.decodeAddress(address_to)
|
||||
|
||||
tx = CTransaction()
|
||||
@@ -2699,16 +2774,14 @@ class BTCInterface(Secp256k1Interface):
|
||||
scriptSig=self.getScriptScriptSig(script_lock),
|
||||
)
|
||||
)
|
||||
tx.vout.append(
|
||||
self.txoType()(cb_swap_value, self.getScriptForPubkeyHash(pkh_to))
|
||||
)
|
||||
tx.vout.append(self.txoType()(spend_value, self.getScriptForPubkeyHash(pkh_to)))
|
||||
|
||||
pay_fee = self.getBLockSpendTxFee(tx, b_fee)
|
||||
tx.vout[0].nValue = cb_swap_value - pay_fee
|
||||
tx.vout[0].nValue = spend_value - pay_fee
|
||||
|
||||
b_lock_spend_tx = tx.serialize()
|
||||
b_lock_spend_tx = self.signTxWithKey(
|
||||
b_lock_spend_tx, kbs, prev_amount=cb_swap_value
|
||||
b_lock_spend_tx, kbs, prev_amount=spend_value
|
||||
)
|
||||
|
||||
return bytes.fromhex(self.publishTx(b_lock_spend_tx))
|
||||
@@ -3036,9 +3109,7 @@ class BTCInterface(Secp256k1Interface):
|
||||
return self._getOutputElectrum(txid, dest_script, expect_value, xmr_swap)
|
||||
|
||||
# TODO: Use getrawtransaction if txindex is active
|
||||
utxos = self.rpc(
|
||||
"scantxoutset", ["start", ["raw({})".format(dest_script.hex())]]
|
||||
)
|
||||
utxos = self._rpc_scantxoutset(["raw({})".format(dest_script.hex())])
|
||||
if "height" in utxos: # chain_height not returned by v18 codebase
|
||||
chain_height = utxos["height"]
|
||||
else:
|
||||
@@ -3492,13 +3563,12 @@ class BTCInterface(Secp256k1Interface):
|
||||
|
||||
sum_unspent = 0
|
||||
|
||||
with BTCInterface._scantxoutset_lock:
|
||||
self._log.debug("scantxoutset start")
|
||||
ro = self.rpc("scantxoutset", ["start", ["addr({})".format(address)]])
|
||||
self._log.debug("scantxoutset end")
|
||||
self._log.debug("scantxoutset start")
|
||||
ro = self._rpc_scantxoutset(["addr({})".format(address)])
|
||||
self._log.debug("scantxoutset end")
|
||||
|
||||
for o in ro["unspents"]:
|
||||
sum_unspent += self.make_int(o["amount"])
|
||||
for o in ro["unspents"]:
|
||||
sum_unspent += self.make_int(o["amount"])
|
||||
return sum_unspent
|
||||
|
||||
def _getUTXOBalanceElectrum(self, address: str):
|
||||
@@ -3881,7 +3951,7 @@ class BTCInterface(Secp256k1Interface):
|
||||
|
||||
self.rpc("loadwallet", [self._rpc_wallet])
|
||||
|
||||
self.rpc_wallet("encryptwallet", [password])
|
||||
self.rpc_wallet("encryptwallet", [password], timeout=120)
|
||||
|
||||
if check_seed is False or seed_id_before == "Not found" or walletpath is None:
|
||||
return
|
||||
@@ -4005,7 +4075,9 @@ class BTCInterface(Secp256k1Interface):
|
||||
if self.isWalletEncrypted():
|
||||
raise ValueError("Old password must be set")
|
||||
return self.encryptWallet(new_password, check_seed=check_seed_if_encrypt)
|
||||
self.rpc_wallet("walletpassphrasechange", [old_password, new_password])
|
||||
self.rpc_wallet(
|
||||
"walletpassphrasechange", [old_password, new_password], timeout=120
|
||||
)
|
||||
|
||||
def unlockWallet(self, password: str, check_seed: bool = True) -> None:
|
||||
if password == "":
|
||||
@@ -4038,12 +4110,9 @@ class BTCInterface(Secp256k1Interface):
|
||||
|
||||
try:
|
||||
seed_id = self.getWalletSeedID()
|
||||
self._log.debug(
|
||||
f"{self.ticker()} unlockWallet getWalletSeedID returned: {seed_id}"
|
||||
)
|
||||
needs_seed_init = seed_id == "Not found"
|
||||
except Exception as e:
|
||||
self._log.debug(f"getWalletSeedID failed: {e}, will initialize seed")
|
||||
self._log.debug(f"getWalletSeedID failed: {e}")
|
||||
needs_seed_init = True
|
||||
if needs_seed_init:
|
||||
self._log.info(f"Initializing HD seed for {self.coin_name()}.")
|
||||
@@ -4051,11 +4120,9 @@ class BTCInterface(Secp256k1Interface):
|
||||
if password:
|
||||
self._log.info(f"Encrypting {self.coin_name()} wallet.")
|
||||
try:
|
||||
self.rpc_wallet("encryptwallet", [password])
|
||||
self.rpc_wallet("encryptwallet", [password], timeout=120)
|
||||
except Exception as e:
|
||||
self._log.debug(f"encryptwallet returned: {e}")
|
||||
import time
|
||||
|
||||
for i in range(10):
|
||||
time.sleep(1)
|
||||
try:
|
||||
@@ -4072,7 +4139,7 @@ class BTCInterface(Secp256k1Interface):
|
||||
check_seed = False
|
||||
|
||||
if self.isWalletEncrypted():
|
||||
self.rpc_wallet("walletpassphrase", [password, 100000000])
|
||||
self.rpc_wallet("walletpassphrase", [password, 100000000], timeout=120)
|
||||
if check_seed:
|
||||
self._sc.checkWalletSeed(self.coin_type())
|
||||
|
||||
@@ -4080,7 +4147,15 @@ class BTCInterface(Secp256k1Interface):
|
||||
self._log.info(f"lockWallet - {self.ticker()}")
|
||||
if self.useBackend():
|
||||
return
|
||||
self.rpc_wallet("walletlock")
|
||||
try:
|
||||
self.rpc_wallet("walletlock")
|
||||
except Exception as e:
|
||||
if "unencrypted wallet" in str(e).lower():
|
||||
self._log.debug(
|
||||
f"lockWallet skipped - {self.ticker()} wallet is not encrypted"
|
||||
)
|
||||
return
|
||||
raise
|
||||
|
||||
def get_p2sh_script_pubkey(self, script: bytearray) -> bytearray:
|
||||
script_hash = hash160(script)
|
||||
|
||||
@@ -132,7 +132,7 @@ class DASHInterface(BTCInterface):
|
||||
self.unlockWallet(old_password, check_seed=False)
|
||||
seed_id_before: str = self.getWalletSeedID()
|
||||
|
||||
self.rpc_wallet("encryptwallet", [new_password])
|
||||
self.rpc_wallet("encryptwallet", [new_password], timeout=120)
|
||||
|
||||
if check_seed is False or seed_id_before == "Not found":
|
||||
return
|
||||
@@ -156,4 +156,6 @@ class DASHInterface(BTCInterface):
|
||||
if self.isWalletEncrypted():
|
||||
raise ValueError("Old password must be set")
|
||||
return self.encryptWallet(old_password, new_password, check_seed_if_encrypt)
|
||||
self.rpc_wallet("walletpassphrasechange", [old_password, new_password])
|
||||
self.rpc_wallet(
|
||||
"walletpassphrasechange", [old_password, new_password], timeout=120
|
||||
)
|
||||
|
||||
@@ -188,6 +188,10 @@ class DCRInterface(Secp256k1Interface):
|
||||
def coin_type():
|
||||
return Coins.DCR
|
||||
|
||||
@staticmethod
|
||||
def useBackend() -> bool:
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def exp() -> int:
|
||||
return 8
|
||||
@@ -364,7 +368,9 @@ class DCRInterface(Secp256k1Interface):
|
||||
# Read initial pwd from settings
|
||||
settings = self._sc.getChainClientSettings(self.coin_type())
|
||||
old_password = settings["wallet_pwd"]
|
||||
self.rpc_wallet("walletpassphrasechange", [old_password, new_password])
|
||||
self.rpc_wallet(
|
||||
"walletpassphrasechange", [old_password, new_password], timeout=120
|
||||
)
|
||||
|
||||
# Lock wallet to match other coins
|
||||
self.rpc_wallet("walletlock")
|
||||
@@ -378,7 +384,7 @@ class DCRInterface(Secp256k1Interface):
|
||||
self._log.info("unlockWallet - {}".format(self.ticker()))
|
||||
|
||||
# Max timeout value, ~3 years
|
||||
self.rpc_wallet("walletpassphrase", [password, 100000000])
|
||||
self.rpc_wallet("walletpassphrase", [password, 100000000], timeout=120)
|
||||
if check_seed:
|
||||
self._sc.checkWalletSeed(self.coin_type())
|
||||
|
||||
@@ -1064,6 +1070,9 @@ class DCRInterface(Secp256k1Interface):
|
||||
def describeTx(self, tx_hex: str):
|
||||
return self.rpc("decoderawtransaction", [tx_hex])
|
||||
|
||||
def decodeRawTransaction(self, tx_hex: str):
|
||||
return self.rpc("decoderawtransaction", [tx_hex])
|
||||
|
||||
def fundTx(self, tx: bytes, feerate) -> bytes:
|
||||
feerate_str = float(self.format_amount(feerate))
|
||||
# TODO: unlock unspents if bid cancelled
|
||||
@@ -1732,15 +1741,19 @@ class DCRInterface(Secp256k1Interface):
|
||||
tx.vout.append(self.txoType()(output_amount, script_pk))
|
||||
return tx.serialize()
|
||||
|
||||
def publishBLockTx(
|
||||
self, kbv, Kbs, output_amount, feerate, unlock_time: int = 0
|
||||
) -> bytes:
|
||||
def publishBLockTx(self, kbv, Kbs, output_amount, feerate, unlock_time: int = 0):
|
||||
b_lock_tx = self.createBLockTx(Kbs, output_amount)
|
||||
|
||||
b_lock_tx = self.fundTx(b_lock_tx, feerate)
|
||||
|
||||
script_pk = self.getPkDest(Kbs)
|
||||
funded_tx = self.loadTx(b_lock_tx)
|
||||
lock_vout = findOutput(funded_tx, script_pk)
|
||||
|
||||
b_lock_tx = self.signTxWithWallet(b_lock_tx)
|
||||
|
||||
return bytes.fromhex(self.publishTx(b_lock_tx))
|
||||
txid = bytes.fromhex(self.publishTx(b_lock_tx))
|
||||
return txid, lock_vout
|
||||
|
||||
def getBLockSpendTxFee(self, tx, fee_rate: int) -> int:
|
||||
witness_bytes = 115
|
||||
@@ -1764,26 +1777,53 @@ class DCRInterface(Secp256k1Interface):
|
||||
lock_tx_vout=None,
|
||||
) -> bytes:
|
||||
self._log.info("spendBLockTx %s:\n", chain_b_lock_txid.hex())
|
||||
locked_n = lock_tx_vout
|
||||
|
||||
Kbs = self.getPubkey(kbs)
|
||||
script_pk = self.getPkDest(Kbs)
|
||||
|
||||
if locked_n is None:
|
||||
self._log.debug(
|
||||
f"Unknown lock vout, searching tx: {chain_b_lock_txid.hex()}"
|
||||
locked_n = None
|
||||
actual_value = None
|
||||
wtx = self.rpc_wallet(
|
||||
"gettransaction",
|
||||
[
|
||||
chain_b_lock_txid.hex(),
|
||||
],
|
||||
)
|
||||
lock_tx = self.loadTx(bytes.fromhex(wtx["hex"]))
|
||||
locked_n = findOutput(lock_tx, script_pk)
|
||||
if locked_n is not None:
|
||||
actual_value = lock_tx.vout[locked_n].value
|
||||
else:
|
||||
self._log.error(
|
||||
f"spendBLockTx: Output not found in tx {chain_b_lock_txid.hex()}, "
|
||||
f"script_pk={script_pk.hex()}, num_outputs={len(lock_tx.vout)}"
|
||||
)
|
||||
# When refunding a lock tx, it should be in the wallet as a sent tx
|
||||
wtx = self.rpc_wallet(
|
||||
"gettransaction",
|
||||
[
|
||||
chain_b_lock_txid.hex(),
|
||||
],
|
||||
for i, out in enumerate(lock_tx.vout):
|
||||
self._log.debug(
|
||||
f" vout[{i}]: value={out.value}, scriptPubKey={out.scriptPubKey.hex()}"
|
||||
)
|
||||
|
||||
if (
|
||||
locked_n is not None
|
||||
and lock_tx_vout is not None
|
||||
and locked_n != lock_tx_vout
|
||||
):
|
||||
self._log.warning(
|
||||
f"spendBLockTx: Stored vout {lock_tx_vout} differs from actual vout {locked_n} "
|
||||
f"for tx {chain_b_lock_txid.hex()}"
|
||||
)
|
||||
lock_tx = self.loadTx(bytes.fromhex(wtx["hex"]))
|
||||
locked_n = findOutput(lock_tx, script_pk)
|
||||
|
||||
ensure(locked_n is not None, "Output not found in tx")
|
||||
|
||||
spend_value = cb_swap_value
|
||||
if spend_actual_balance and actual_value is not None:
|
||||
if actual_value != cb_swap_value:
|
||||
self._log.warning(
|
||||
f"spendBLockTx: Spending actual balance {actual_value}, "
|
||||
f"not expected swap value {cb_swap_value}."
|
||||
)
|
||||
spend_value = actual_value
|
||||
|
||||
pkh_to = self.decodeAddress(address_to)
|
||||
|
||||
tx = CTransaction()
|
||||
@@ -1792,10 +1832,10 @@ class DCRInterface(Secp256k1Interface):
|
||||
chain_b_lock_txid_int = b2i(chain_b_lock_txid)
|
||||
|
||||
tx.vin.append(CTxIn(COutPoint(chain_b_lock_txid_int, locked_n, 0), sequence=0))
|
||||
tx.vout.append(self.txoType()(cb_swap_value, self.getPubkeyHashDest(pkh_to)))
|
||||
tx.vout.append(self.txoType()(spend_value, self.getPubkeyHashDest(pkh_to)))
|
||||
|
||||
pay_fee = self.getBLockSpendTxFee(tx, b_fee)
|
||||
tx.vout[0].value = cb_swap_value - pay_fee
|
||||
tx.vout[0].value = spend_value - pay_fee
|
||||
|
||||
b_lock_spend_tx = tx.serialize()
|
||||
b_lock_spend_tx = self.signTxWithKey(b_lock_spend_tx, kbs)
|
||||
|
||||
@@ -119,7 +119,8 @@ class ElectrumConnection:
|
||||
self._socket = None
|
||||
self._connected = False
|
||||
_close_socket_safe(sock)
|
||||
for q in self._response_queues.values():
|
||||
queues = list(self._response_queues.values())
|
||||
for q in queues:
|
||||
try:
|
||||
q.put({"error": "Connection closed"})
|
||||
except Exception:
|
||||
@@ -305,17 +306,26 @@ class ElectrumConnection:
|
||||
results = {}
|
||||
deadline = time.time() + timeout
|
||||
for req_id in expected_ids:
|
||||
remaining = deadline - time.time()
|
||||
if remaining <= 0:
|
||||
raise TemporaryError("Batch request timed out")
|
||||
response = None
|
||||
while response is None:
|
||||
remaining = deadline - time.time()
|
||||
if remaining <= 0:
|
||||
raise TemporaryError("Batch request timed out")
|
||||
if not self._connected:
|
||||
raise TemporaryError("Connection closed during batch request")
|
||||
poll_time = min(remaining, 2.0)
|
||||
try:
|
||||
response = self._response_queues[req_id].get(timeout=poll_time)
|
||||
except queue.Empty:
|
||||
continue
|
||||
try:
|
||||
response = self._response_queues[req_id].get(timeout=remaining)
|
||||
if "error" in response and response["error"]:
|
||||
error_msg = str(response["error"])
|
||||
if "Connection closed" in error_msg:
|
||||
raise TemporaryError("Connection closed during batch request")
|
||||
results[req_id] = {"error": response["error"]}
|
||||
else:
|
||||
results[req_id] = {"result": response.get("result")}
|
||||
except queue.Empty:
|
||||
raise TemporaryError("Batch request timed out")
|
||||
finally:
|
||||
self._response_queues.pop(req_id, None)
|
||||
return results
|
||||
@@ -329,13 +339,13 @@ class ElectrumConnection:
|
||||
self._request_id += 1
|
||||
request_id = self._request_id
|
||||
self._response_queues[request_id] = queue.Queue()
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": request_id,
|
||||
"method": method,
|
||||
"params": params if params else [],
|
||||
}
|
||||
self._socket.sendall((json.dumps(request) + "\n").encode())
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": request_id,
|
||||
"method": method,
|
||||
"params": params if params else [],
|
||||
}
|
||||
self._socket.sendall((json.dumps(request) + "\n").encode())
|
||||
result = self._receive_response_async(request_id, timeout=timeout)
|
||||
return result
|
||||
else:
|
||||
@@ -470,6 +480,7 @@ class ElectrumServer:
|
||||
self._connection = None
|
||||
self._current_server_idx = 0
|
||||
self._lock = threading.Lock()
|
||||
self._stopping = False
|
||||
|
||||
self._server_version = None
|
||||
self._current_server_host = None
|
||||
@@ -492,17 +503,24 @@ class ElectrumServer:
|
||||
self._server_blacklist = {}
|
||||
self._rate_limit_backoff = 300
|
||||
|
||||
self._consecutive_timeouts = 0
|
||||
self._max_consecutive_timeouts = 5
|
||||
self._last_timeout_time = 0
|
||||
self._timeout_decay_seconds = 90
|
||||
|
||||
self._keepalive_thread = None
|
||||
self._keepalive_running = False
|
||||
self._keepalive_interval = 15
|
||||
self._last_activity = 0
|
||||
self._last_reconnect_time = 0
|
||||
|
||||
self._min_request_interval = 0.02
|
||||
self._last_request_time = 0
|
||||
|
||||
self._bg_connection = None
|
||||
self._bg_lock = threading.Lock()
|
||||
self._bg_last_activity = 0
|
||||
self._user_connection = None
|
||||
self._user_lock = threading.Lock()
|
||||
self._user_last_activity = 0
|
||||
self._user_connection_logged = False
|
||||
|
||||
self._subscribed_height = 0
|
||||
self._subscribed_height_time = 0
|
||||
@@ -559,6 +577,8 @@ class ElectrumServer:
|
||||
return self._servers[index % len(self._servers)]
|
||||
|
||||
def connect(self):
|
||||
if self._stopping:
|
||||
return
|
||||
sorted_servers = self.get_sorted_servers()
|
||||
for server in sorted_servers:
|
||||
try:
|
||||
@@ -576,6 +596,8 @@ class ElectrumServer:
|
||||
version_info = conn.get_server_version()
|
||||
if version_info and len(version_info) > 0:
|
||||
self._server_version = version_info[0]
|
||||
prev_host = self._current_server_host
|
||||
prev_port = self._current_server_port
|
||||
self._current_server_host = server["host"]
|
||||
self._current_server_port = server["port"]
|
||||
self._connection = conn
|
||||
@@ -585,6 +607,7 @@ class ElectrumServer:
|
||||
self._all_servers_failed = False
|
||||
self._update_server_score(server, success=True, latency_ms=connect_time)
|
||||
self._last_activity = time.time()
|
||||
self._last_reconnect_time = time.time()
|
||||
if self._log:
|
||||
if not self._initial_connection_logged:
|
||||
self._log.info(
|
||||
@@ -592,11 +615,15 @@ class ElectrumServer:
|
||||
f"({self._server_version}, {connect_time:.0f}ms)"
|
||||
)
|
||||
self._initial_connection_logged = True
|
||||
else:
|
||||
self._log.debug(
|
||||
f"Reconnected to Electrum server: {server['host']}:{server['port']} "
|
||||
elif server["host"] != prev_host or server["port"] != prev_port:
|
||||
self._log.info(
|
||||
f"Switched to Electrum server: {server['host']}:{server['port']} "
|
||||
f"({connect_time:.0f}ms)"
|
||||
)
|
||||
if self._stopping:
|
||||
conn.disconnect()
|
||||
self._connection = None
|
||||
return
|
||||
if self._realtime_enabled:
|
||||
self._start_realtime_listener()
|
||||
self._start_keepalive()
|
||||
@@ -609,8 +636,6 @@ class ElectrumServer:
|
||||
self._update_server_score(server, success=False)
|
||||
if self._is_rate_limit_error(str(e)):
|
||||
self._blacklist_server(server, str(e))
|
||||
if self._log:
|
||||
self._log.debug(f"Failed to connect to {server['host']}: {e}")
|
||||
continue
|
||||
self._all_servers_failed = True
|
||||
raise TemporaryError(
|
||||
@@ -673,11 +698,6 @@ class ElectrumServer:
|
||||
key = self._get_server_key(s)
|
||||
if key in self._server_blacklist:
|
||||
if now < self._server_blacklist[key]:
|
||||
if self._log:
|
||||
remaining = int(self._server_blacklist[key] - now)
|
||||
self._log.debug(
|
||||
f"Skipping blacklisted server {key} ({remaining}s remaining)"
|
||||
)
|
||||
continue
|
||||
else:
|
||||
del self._server_blacklist[key]
|
||||
@@ -728,15 +748,14 @@ class ElectrumServer:
|
||||
if self._connection:
|
||||
self._connection._start_listener()
|
||||
result = self._connection.call(
|
||||
"blockchain.headers.subscribe", [], timeout=10
|
||||
"blockchain.headers.subscribe", [], timeout=20
|
||||
)
|
||||
if result and isinstance(result, dict):
|
||||
height = result.get("height", 0)
|
||||
if height > 0:
|
||||
self._on_header_update(height)
|
||||
except Exception as e:
|
||||
if self._log:
|
||||
self._log.debug(f"Failed to subscribe to headers: {e}")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def register_height_callback(self, callback):
|
||||
self._height_callback = callback
|
||||
@@ -744,6 +763,11 @@ class ElectrumServer:
|
||||
def get_subscribed_height(self) -> int:
|
||||
return self._subscribed_height
|
||||
|
||||
def recently_reconnected(self, grace_seconds: int = 30) -> bool:
|
||||
if self._last_reconnect_time == 0:
|
||||
return False
|
||||
return (time.time() - self._last_reconnect_time) < grace_seconds
|
||||
|
||||
def get_server_scores(self) -> dict:
|
||||
return {
|
||||
self._get_server_key(s): {
|
||||
@@ -781,7 +805,8 @@ class ElectrumServer:
|
||||
return
|
||||
time.sleep(1)
|
||||
|
||||
if time.time() - self._last_activity >= self._keepalive_interval:
|
||||
now = time.time()
|
||||
if now - self._last_activity >= self._keepalive_interval:
|
||||
if self._connection and self._connection.is_connected():
|
||||
if self._lock.acquire(blocking=False):
|
||||
try:
|
||||
@@ -802,6 +827,8 @@ class ElectrumServer:
|
||||
self._last_request_time = time.time()
|
||||
|
||||
def _retry_on_failure(self):
|
||||
if self._stopping:
|
||||
return
|
||||
self._current_server_idx = (self._current_server_idx + 1) % len(self._servers)
|
||||
if self._connection:
|
||||
try:
|
||||
@@ -824,17 +851,27 @@ class ElectrumServer:
|
||||
return False
|
||||
|
||||
def call(self, method, params=None, timeout=10):
|
||||
if self._stopping:
|
||||
raise TemporaryError("Electrum server is shutting down")
|
||||
self._throttle_request()
|
||||
lock_acquired = self._lock.acquire(timeout=timeout + 5)
|
||||
if not lock_acquired:
|
||||
raise TemporaryError(f"Electrum call timed out waiting for lock: {method}")
|
||||
try:
|
||||
for attempt in range(2):
|
||||
if self._stopping:
|
||||
raise TemporaryError("Electrum server is shutting down")
|
||||
if self._connection is None or not self._connection.is_connected():
|
||||
self.connect()
|
||||
if self._connection is None:
|
||||
raise TemporaryError("Failed to establish Electrum connection")
|
||||
elif (time.time() - self._last_activity) > 60:
|
||||
if not self._check_connection_health():
|
||||
self._retry_on_failure()
|
||||
if self._connection is None:
|
||||
raise TemporaryError(
|
||||
"Failed to re-establish Electrum connection"
|
||||
)
|
||||
try:
|
||||
result = self._connection.call(method, params, timeout=timeout)
|
||||
self._last_activity = time.time()
|
||||
@@ -851,17 +888,27 @@ class ElectrumServer:
|
||||
self._lock.release()
|
||||
|
||||
def call_batch(self, requests, timeout=15):
|
||||
if self._stopping:
|
||||
raise TemporaryError("Electrum server is shutting down")
|
||||
self._throttle_request()
|
||||
lock_acquired = self._lock.acquire(timeout=timeout + 5)
|
||||
if not lock_acquired:
|
||||
raise TemporaryError("Electrum batch call timed out waiting for lock")
|
||||
try:
|
||||
for attempt in range(2):
|
||||
if self._stopping:
|
||||
raise TemporaryError("Electrum server is shutting down")
|
||||
if self._connection is None or not self._connection.is_connected():
|
||||
self.connect()
|
||||
if self._connection is None:
|
||||
raise TemporaryError("Failed to establish Electrum connection")
|
||||
elif (time.time() - self._last_activity) > 60:
|
||||
if not self._check_connection_health():
|
||||
self._retry_on_failure()
|
||||
if self._connection is None:
|
||||
raise TemporaryError(
|
||||
"Failed to re-establish Electrum connection"
|
||||
)
|
||||
try:
|
||||
result = self._connection.call_batch(requests)
|
||||
self._last_activity = time.time()
|
||||
@@ -877,7 +924,9 @@ class ElectrumServer:
|
||||
finally:
|
||||
self._lock.release()
|
||||
|
||||
def _connect_background(self):
|
||||
def _connect_user(self):
|
||||
if self._stopping:
|
||||
return False
|
||||
sorted_servers = self.get_sorted_servers()
|
||||
for server in sorted_servers:
|
||||
try:
|
||||
@@ -890,105 +939,213 @@ class ElectrumServer:
|
||||
proxy_port=self._proxy_port,
|
||||
)
|
||||
conn.connect()
|
||||
self._bg_connection = conn
|
||||
conn.get_server_version()
|
||||
self._user_connection = conn
|
||||
self._user_last_activity = time.time()
|
||||
if self._log:
|
||||
self._log.debug(
|
||||
f"Background connection established to {server['host']}"
|
||||
)
|
||||
if not self._user_connection_logged:
|
||||
self._log.debug(
|
||||
f"User connection established to {server['host']}"
|
||||
)
|
||||
self._user_connection_logged = True
|
||||
else:
|
||||
self._log.debug(
|
||||
f"User connection reconnected to {server['host']}"
|
||||
)
|
||||
return True
|
||||
except Exception as e:
|
||||
if self._log:
|
||||
self._log.debug(
|
||||
f"Background connection failed to {server['host']}: {e}"
|
||||
)
|
||||
self._log.debug(f"User connection failed to {server['host']}: {e}")
|
||||
continue
|
||||
return False
|
||||
|
||||
def call_background(self, method, params=None, timeout=10):
|
||||
lock_acquired = self._bg_lock.acquire(timeout=1)
|
||||
def _record_timeout(self):
|
||||
if self._stopping:
|
||||
return
|
||||
now = time.time()
|
||||
if (
|
||||
now - self._last_timeout_time
|
||||
) > self._timeout_decay_seconds and self._last_timeout_time > 0:
|
||||
self._consecutive_timeouts = 0
|
||||
self._consecutive_timeouts += 1
|
||||
self._last_timeout_time = now
|
||||
if self._consecutive_timeouts >= self._max_consecutive_timeouts:
|
||||
server = self._get_server(self._current_server_idx)
|
||||
reason = f"{self._consecutive_timeouts} consecutive timeouts"
|
||||
self._blacklist_server(server, reason)
|
||||
self._consecutive_timeouts = 0
|
||||
self._last_timeout_time = 0
|
||||
try:
|
||||
self._retry_on_failure()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def call_background(self, method, params=None, timeout=20):
|
||||
if self._stopping:
|
||||
raise TemporaryError("Electrum server is shutting down")
|
||||
conn = self._connection
|
||||
if conn is None or not conn.is_connected():
|
||||
if self._stopping:
|
||||
raise TemporaryError("Electrum server is shutting down")
|
||||
try:
|
||||
self.connect()
|
||||
conn = self._connection
|
||||
except Exception:
|
||||
raise TemporaryError("Electrum call failed: no connection")
|
||||
if conn is None or not conn.is_connected():
|
||||
raise TemporaryError("Electrum call failed: no connection")
|
||||
try:
|
||||
result = conn.call(method, params, timeout=timeout)
|
||||
self._last_activity = time.time()
|
||||
return result
|
||||
except TemporaryError as e:
|
||||
if self._stopping:
|
||||
raise TemporaryError("Electrum server is shutting down")
|
||||
if "timed out" in str(e).lower():
|
||||
self._record_timeout()
|
||||
raise
|
||||
|
||||
def call_batch_background(self, requests, timeout=30):
|
||||
if self._stopping:
|
||||
raise TemporaryError("Electrum server is shutting down")
|
||||
conn = self._connection
|
||||
if conn is None or not conn.is_connected():
|
||||
if self._stopping:
|
||||
raise TemporaryError("Electrum server is shutting down")
|
||||
self._record_timeout()
|
||||
conn = self._connection
|
||||
if conn is None or not conn.is_connected():
|
||||
try:
|
||||
self.connect()
|
||||
conn = self._connection
|
||||
except Exception:
|
||||
raise TemporaryError("Electrum batch call failed: no connection")
|
||||
if conn is None or not conn.is_connected():
|
||||
raise TemporaryError("Electrum batch call failed: no connection")
|
||||
try:
|
||||
result = conn.call_batch(requests)
|
||||
self._last_activity = time.time()
|
||||
return result
|
||||
except TemporaryError as e:
|
||||
if self._stopping:
|
||||
raise TemporaryError("Electrum server is shutting down")
|
||||
if "timed out" in str(e).lower():
|
||||
self._record_timeout()
|
||||
raise
|
||||
|
||||
def call_user(self, method, params=None, timeout=10):
|
||||
if self._stopping:
|
||||
raise TemporaryError("Electrum server is shutting down")
|
||||
lock_acquired = self._user_lock.acquire(timeout=timeout + 2)
|
||||
if not lock_acquired:
|
||||
return self.call(method, params, timeout)
|
||||
raise TemporaryError(f"User connection busy: {method}")
|
||||
|
||||
try:
|
||||
if self._bg_connection is None or not self._bg_connection.is_connected():
|
||||
if not self._connect_background():
|
||||
self._bg_lock.release()
|
||||
return self.call(method, params, timeout)
|
||||
if (
|
||||
self._user_connection is None
|
||||
or not self._user_connection.is_connected()
|
||||
):
|
||||
if not self._connect_user():
|
||||
raise TemporaryError("User connection unavailable")
|
||||
|
||||
try:
|
||||
result = self._bg_connection.call(method, params, timeout=timeout)
|
||||
self._bg_last_activity = time.time()
|
||||
result = self._user_connection.call(method, params, timeout=timeout)
|
||||
self._user_last_activity = time.time()
|
||||
return result
|
||||
except Exception:
|
||||
if self._bg_connection:
|
||||
except Exception as e:
|
||||
if self._log:
|
||||
self._log.debug(f"User call failed ({method}): {e}")
|
||||
if self._user_connection:
|
||||
try:
|
||||
self._bg_connection.disconnect()
|
||||
self._user_connection.disconnect()
|
||||
except Exception:
|
||||
pass
|
||||
self._bg_connection = None
|
||||
self._user_connection = None
|
||||
|
||||
if self._connect_background():
|
||||
if self._connect_user():
|
||||
try:
|
||||
result = self._bg_connection.call(
|
||||
result = self._user_connection.call(
|
||||
method, params, timeout=timeout
|
||||
)
|
||||
self._bg_last_activity = time.time()
|
||||
self._user_last_activity = time.time()
|
||||
return result
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as e2:
|
||||
raise TemporaryError(f"User call failed: {e2}")
|
||||
|
||||
return self.call(method, params, timeout)
|
||||
raise TemporaryError(f"User call failed: {e}")
|
||||
finally:
|
||||
self._bg_lock.release()
|
||||
self._user_lock.release()
|
||||
|
||||
def call_batch_background(self, requests, timeout=15):
|
||||
lock_acquired = self._bg_lock.acquire(timeout=1)
|
||||
def call_batch_user(self, requests, timeout=15):
|
||||
if self._stopping:
|
||||
raise TemporaryError("Electrum server is shutting down")
|
||||
lock_acquired = self._user_lock.acquire(timeout=timeout + 2)
|
||||
if not lock_acquired:
|
||||
return self.call_batch(requests, timeout)
|
||||
raise TemporaryError("User connection busy")
|
||||
|
||||
try:
|
||||
if self._bg_connection is None or not self._bg_connection.is_connected():
|
||||
if not self._connect_background():
|
||||
self._bg_lock.release()
|
||||
return self.call_batch(requests, timeout)
|
||||
if (
|
||||
self._user_connection is None
|
||||
or not self._user_connection.is_connected()
|
||||
):
|
||||
if not self._connect_user():
|
||||
raise TemporaryError("User connection unavailable")
|
||||
|
||||
try:
|
||||
result = self._bg_connection.call_batch(requests)
|
||||
self._bg_last_activity = time.time()
|
||||
result = self._user_connection.call_batch(requests)
|
||||
self._user_last_activity = time.time()
|
||||
return result
|
||||
except Exception:
|
||||
if self._bg_connection:
|
||||
except Exception as e:
|
||||
if self._log:
|
||||
self._log.debug(f"User batch call failed: {e}")
|
||||
if self._user_connection:
|
||||
try:
|
||||
self._bg_connection.disconnect()
|
||||
self._user_connection.disconnect()
|
||||
except Exception:
|
||||
pass
|
||||
self._bg_connection = None
|
||||
self._user_connection = None
|
||||
|
||||
if self._connect_background():
|
||||
if self._connect_user():
|
||||
try:
|
||||
result = self._bg_connection.call_batch(requests)
|
||||
self._bg_last_activity = time.time()
|
||||
result = self._user_connection.call_batch(requests)
|
||||
self._user_last_activity = time.time()
|
||||
return result
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as e2:
|
||||
raise TemporaryError(f"User batch call failed: {e2}")
|
||||
|
||||
return self.call_batch(requests, timeout)
|
||||
raise TemporaryError(f"User batch call failed: {e}")
|
||||
finally:
|
||||
self._bg_lock.release()
|
||||
self._user_lock.release()
|
||||
|
||||
def disconnect(self):
|
||||
self._stop_keepalive()
|
||||
with self._lock:
|
||||
if self._connection:
|
||||
self._connection.disconnect()
|
||||
self._connection = None
|
||||
with self._bg_lock:
|
||||
if self._bg_connection:
|
||||
lock_acquired = self._lock.acquire(timeout=5)
|
||||
if lock_acquired:
|
||||
try:
|
||||
if self._connection:
|
||||
self._connection.disconnect()
|
||||
self._connection = None
|
||||
finally:
|
||||
self._lock.release()
|
||||
else:
|
||||
conn = self._connection
|
||||
if conn:
|
||||
try:
|
||||
self._bg_connection.disconnect()
|
||||
conn.disconnect()
|
||||
except Exception:
|
||||
pass
|
||||
self._bg_connection = None
|
||||
with self._user_lock:
|
||||
if self._user_connection:
|
||||
try:
|
||||
self._user_connection.disconnect()
|
||||
except Exception:
|
||||
pass
|
||||
self._user_connection = None
|
||||
self._user_connection_logged = False
|
||||
|
||||
def shutdown(self):
|
||||
self._stopping = True
|
||||
self.disconnect()
|
||||
|
||||
def get_balance(self, scripthash):
|
||||
result = self.call("blockchain.scripthash.get_balance", [scripthash])
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2022-2023 tecnovert
|
||||
# Copyright (c) 2024-2025 The Basicswap developers
|
||||
# Copyright (c) 2024-2026 The Basicswap developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
@@ -64,7 +64,7 @@ class FIROInterface(BTCInterface):
|
||||
# Firo shuts down after encryptwallet
|
||||
seed_id_before: str = self.getWalletSeedID() if check_seed else "Not found"
|
||||
|
||||
self.rpc_wallet("encryptwallet", [password])
|
||||
self.rpc_wallet("encryptwallet", [password], timeout=120)
|
||||
|
||||
if check_seed is False or seed_id_before == "Not found":
|
||||
return
|
||||
@@ -102,6 +102,100 @@ class FIROInterface(BTCInterface):
|
||||
return addr_info["ismine"]
|
||||
return addr_info["ismine"] or addr_info["iswatchonly"]
|
||||
|
||||
def getNewSparkAddress(self) -> str:
|
||||
try:
|
||||
return self.rpc_wallet("getnewsparkaddress")[0]
|
||||
except Exception as e:
|
||||
self._log.error(f"getnewsparkaddress failed: {str(e)}")
|
||||
raise
|
||||
|
||||
def getNewStealthAddress(self):
|
||||
"""Get a new Spark address (alias for consistency with other coins)."""
|
||||
return self.getNewSparkAddress()
|
||||
|
||||
def getWalletInfo(self):
|
||||
"""Get wallet info including Spark balance."""
|
||||
rv = super(FIROInterface, self).getWalletInfo()
|
||||
try:
|
||||
spark_balance_info = self.rpc("getsparkbalance")
|
||||
# getsparkbalance returns amounts in atomic units (satoshis)
|
||||
# Field names: availableBalance, unconfirmedBalance, fullBalance
|
||||
confirmed = spark_balance_info.get("availableBalance", 0)
|
||||
unconfirmed = spark_balance_info.get("unconfirmedBalance", 0)
|
||||
full_balance = spark_balance_info.get("fullBalance", 0)
|
||||
# Values are already in atomic units, keep as integers
|
||||
# basicswap.py will format them using format_amount
|
||||
rv["spark_balance"] = confirmed if confirmed else 0
|
||||
rv["spark_unconfirmed"] = unconfirmed if unconfirmed else 0
|
||||
immature = full_balance - confirmed - unconfirmed
|
||||
rv["spark_immature"] = immature if immature > 0 else 0
|
||||
except Exception as e:
|
||||
self._log.warning(f"getsparkbalance failed: {str(e)}")
|
||||
rv["spark_balance"] = 0
|
||||
rv["spark_unconfirmed"] = 0
|
||||
rv["spark_immature"] = 0
|
||||
return rv
|
||||
|
||||
def createUTXO(self, value_sats: int):
|
||||
# Create a new address and send value_sats to it
|
||||
|
||||
spendable_balance = self.getSpendableBalance()
|
||||
if spendable_balance < value_sats:
|
||||
raise ValueError("Balance too low")
|
||||
|
||||
address = self.getNewAddress(self._use_segwit, "create_utxo")
|
||||
return (
|
||||
self.withdrawCoin(self.format_amount(value_sats), "plain", address, False),
|
||||
address,
|
||||
)
|
||||
|
||||
def withdrawCoin(self, value, type_from: str, addr_to: str, subfee: bool) -> str:
|
||||
"""Withdraw coins, supporting both transparent and Spark transactions.
|
||||
|
||||
Args:
|
||||
value: Amount to withdraw
|
||||
type_from: "plain" for transparent, "spark" for Spark
|
||||
addr_to: Destination address
|
||||
subfee: Whether to subtract fee from amount
|
||||
"""
|
||||
type_to = "spark" if addr_to.startswith("sm1") else "plain"
|
||||
|
||||
if "spark" in (type_from, type_to):
|
||||
# RPC format: spendspark {"address": {"amount": ..., "subtractfee": ..., "memo": ...}}
|
||||
# RPC wrapper will serialize this as: {"method": "spendspark", "params": [{...}], ...}
|
||||
try:
|
||||
if type_from == "spark":
|
||||
# Construct params: dict where address is the key, wrapped in array for RPC
|
||||
params = [
|
||||
{"address": addr_to, "amount": value, "subtractfee": subfee}
|
||||
]
|
||||
result = self.rpc_wallet("spendspark", params)
|
||||
else:
|
||||
# Use automintspark to perform a plain -> spark tx of full balance
|
||||
balance = self.rpc_wallet("getbalance")
|
||||
if str(balance) == str(value):
|
||||
result = self.rpc_wallet("automintspark")
|
||||
else:
|
||||
# subfee param is available on plain -> spark transactions
|
||||
mint_params = {"amount": value}
|
||||
if subfee:
|
||||
mint_params["subfee"] = True
|
||||
params = [{addr_to: mint_params}]
|
||||
result = self.rpc_wallet("mintspark", params)
|
||||
# spendspark returns a txid string directly, in a result dict, or as an array
|
||||
if isinstance(result, list) and len(result) > 0:
|
||||
return result[0]
|
||||
if isinstance(result, dict):
|
||||
return result.get("txid", result.get("tx", ""))
|
||||
return result
|
||||
except Exception as e:
|
||||
self._log.error(f"spark tx failed: {str(e)}")
|
||||
raise
|
||||
else:
|
||||
# Use standard sendtoaddress for transparent transactions
|
||||
params = [addr_to, value, "", "", subfee]
|
||||
return self.rpc_wallet("sendtoaddress", params)
|
||||
|
||||
def getSCLockScriptAddress(self, lock_script: bytes) -> str:
|
||||
lock_tx_dest = self.getScriptDest(lock_script)
|
||||
address = self.encodeScriptDest(lock_tx_dest)
|
||||
@@ -252,10 +346,6 @@ class FIROInterface(BTCInterface):
|
||||
assert len(script_hash) == 20
|
||||
return CScript([OP_HASH160, script_hash, OP_EQUAL])
|
||||
|
||||
def withdrawCoin(self, value, addr_to, subfee):
|
||||
params = [addr_to, value, "", "", subfee]
|
||||
return self.rpc("sendtoaddress", params)
|
||||
|
||||
def getWalletSeedID(self):
|
||||
return self.rpc("getwalletinfo")["hdmasterkeyid"]
|
||||
|
||||
|
||||
@@ -209,11 +209,9 @@ class LTCInterface(BTCInterface):
|
||||
|
||||
try:
|
||||
seed_id = self.getWalletSeedID()
|
||||
self._log.debug(f"LTC unlockWallet getWalletSeedID returned: {seed_id}")
|
||||
needs_seed_init = seed_id == "Not found"
|
||||
except Exception as e:
|
||||
|
||||
self._log.debug(f"getWalletSeedID failed: {e}, will initialize seed")
|
||||
self._log.debug(f"getWalletSeedID failed: {e}")
|
||||
needs_seed_init = True
|
||||
if needs_seed_init:
|
||||
self._log.info(f"Initializing HD seed for {self.coin_name()}.")
|
||||
@@ -221,7 +219,7 @@ class LTCInterface(BTCInterface):
|
||||
if password:
|
||||
self._log.info(f"Encrypting {self.coin_name()} wallet.")
|
||||
try:
|
||||
self.rpc_wallet("encryptwallet", [password])
|
||||
self.rpc_wallet("encryptwallet", [password], timeout=120)
|
||||
except Exception as e:
|
||||
self._log.debug(f"encryptwallet returned: {e}")
|
||||
import time
|
||||
@@ -242,7 +240,7 @@ class LTCInterface(BTCInterface):
|
||||
check_seed = False
|
||||
|
||||
if self.isWalletEncrypted():
|
||||
self.rpc_wallet("walletpassphrase", [password, 100000000])
|
||||
self.rpc_wallet("walletpassphrase", [password, 100000000], timeout=120)
|
||||
|
||||
if check_seed:
|
||||
self._sc.checkWalletSeed(self.coin_type())
|
||||
@@ -332,7 +330,7 @@ class LTCInterfaceMWEB(LTCInterface):
|
||||
|
||||
if password is not None:
|
||||
# Max timeout value, ~3 years
|
||||
self.rpc_wallet("walletpassphrase", [password, 100000000])
|
||||
self.rpc_wallet("walletpassphrase", [password, 100000000], timeout=120)
|
||||
|
||||
if self.getWalletSeedID() == "Not found":
|
||||
self._sc.initialiseWallet(self.interface_type())
|
||||
@@ -341,7 +339,7 @@ class LTCInterfaceMWEB(LTCInterface):
|
||||
self.rpc("unloadwallet", ["mweb"])
|
||||
self.rpc("loadwallet", ["mweb"])
|
||||
if password is not None:
|
||||
self.rpc_wallet("walletpassphrase", [password, 100000000])
|
||||
self.rpc_wallet("walletpassphrase", [password, 100000000], timeout=120)
|
||||
self.rpc_wallet("keypoolrefill")
|
||||
|
||||
def unlockWallet(self, password: str, check_seed: bool = True) -> None:
|
||||
@@ -355,15 +353,12 @@ class LTCInterfaceMWEB(LTCInterface):
|
||||
if not self.has_mweb_wallet():
|
||||
self.init_wallet(password)
|
||||
else:
|
||||
self.rpc_wallet("walletpassphrase", [password, 100000000])
|
||||
self.rpc_wallet("walletpassphrase", [password, 100000000], timeout=120)
|
||||
try:
|
||||
seed_id = self.getWalletSeedID()
|
||||
self._log.debug(
|
||||
f"LTC_MWEB unlockWallet getWalletSeedID returned: {seed_id}"
|
||||
)
|
||||
needs_seed_init = seed_id == "Not found"
|
||||
except Exception as e:
|
||||
self._log.debug(f"getWalletSeedID failed: {e}, will initialize seed")
|
||||
self._log.debug(f"getWalletSeedID failed: {e}")
|
||||
needs_seed_init = True
|
||||
if needs_seed_init:
|
||||
self._log.info(f"Initializing HD seed for {self.coin_name()}.")
|
||||
|
||||
@@ -40,7 +40,7 @@ class PIVXInterface(BTCInterface):
|
||||
|
||||
seed_id_before: str = self.getWalletSeedID()
|
||||
|
||||
self.rpc_wallet("encryptwallet", [password])
|
||||
self.rpc_wallet("encryptwallet", [password], timeout=120)
|
||||
|
||||
if check_seed is False or seed_id_before == "Not found":
|
||||
return
|
||||
|
||||
+50
-21
@@ -79,9 +79,11 @@ def withdraw_coin(swap_client, coin_type, post_string, is_json):
|
||||
txid_hex = swap_client.withdrawParticl(
|
||||
type_from, type_to, value, address, subfee
|
||||
)
|
||||
elif coin_type == Coins.LTC:
|
||||
elif coin_type in (Coins.LTC, Coins.FIRO):
|
||||
type_from = get_data_entry_or(post_data, "type_from", "plain")
|
||||
txid_hex = swap_client.withdrawLTC(type_from, value, address, subfee)
|
||||
txid_hex = swap_client.withdrawCoinExtended(
|
||||
coin_type, type_from, value, address, subfee
|
||||
)
|
||||
elif coin_type in (Coins.XMR, Coins.WOW):
|
||||
txid_hex = swap_client.withdrawCoin(coin_type, value, address, sweepall)
|
||||
else:
|
||||
@@ -181,6 +183,15 @@ def js_walletbalances(self, url_split, post_string, is_json) -> bytes:
|
||||
version = ci.getDaemonVersion()
|
||||
if version:
|
||||
coin_entry["version"] = version
|
||||
if (
|
||||
v["connection_type"] == "electrum"
|
||||
and hasattr(ci, "_backend")
|
||||
and ci._backend
|
||||
and hasattr(ci._backend, "getSyncStatus")
|
||||
):
|
||||
sync_status = ci._backend.getSyncStatus()
|
||||
coin_entry["electrum_synced"] = sync_status.get("synced", False)
|
||||
coin_entry["electrum_height"] = sync_status.get("height", 0)
|
||||
|
||||
coins_with_balances.append(coin_entry)
|
||||
|
||||
@@ -1254,7 +1265,6 @@ def js_getcoinseed(self, url_split, post_string, is_json) -> bytes:
|
||||
wallet_seed_id = f"Error: {e}"
|
||||
rv.update(
|
||||
{
|
||||
"seed": seed_key.hex(),
|
||||
"seed_id": seed_id.hex(),
|
||||
"expected_seed_id": "Unset" if expect_seedid is None else expect_seedid,
|
||||
"current_seed_id": wallet_seed_id,
|
||||
@@ -1739,38 +1749,57 @@ def js_modeswitchinfo(self, url_split, post_string, is_json) -> bytes:
|
||||
}
|
||||
|
||||
if direction == "lite":
|
||||
legacy_balance_sats = 0
|
||||
has_legacy_funds = False
|
||||
non_bip84_balance_sats = 0
|
||||
has_non_bip84_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:
|
||||
wm = swap_client.getWalletManager()
|
||||
|
||||
bip84_addresses = set()
|
||||
if wm:
|
||||
try:
|
||||
all_addrs = wm.getAllAddresses(
|
||||
coin_type, include_watch_only=False
|
||||
)
|
||||
bip84_addresses = set(all_addrs)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
for u in unspent:
|
||||
addr = u.get("address")
|
||||
if not addr:
|
||||
continue
|
||||
amount_sats = ci.make_int(u.get("amount", 0))
|
||||
if amount_sats <= 0:
|
||||
continue
|
||||
|
||||
if addr not in bip84_addresses:
|
||||
non_bip84_balance_sats += amount_sats
|
||||
has_non_bip84_funds = True
|
||||
except Exception as e:
|
||||
swap_client.log.debug(f"Error checking non-BIP84 addresses: {e}")
|
||||
|
||||
if has_non_bip84_funds and non_bip84_balance_sats > min_viable:
|
||||
rv["show_transfer_option"] = True
|
||||
rv["require_transfer"] = True
|
||||
rv["legacy_balance_sats"] = legacy_balance_sats
|
||||
rv["legacy_balance"] = ci.format_amount(legacy_balance_sats)
|
||||
rv["legacy_balance_sats"] = non_bip84_balance_sats
|
||||
rv["legacy_balance"] = ci.format_amount(non_bip84_balance_sats)
|
||||
rv["message"] = (
|
||||
"Funds on legacy addresses must be transferred for external wallet compatibility"
|
||||
"Funds on non-derivable addresses must be transferred for external wallet compatibility"
|
||||
)
|
||||
else:
|
||||
rv["show_transfer_option"] = False
|
||||
rv["require_transfer"] = False
|
||||
if has_legacy_funds:
|
||||
rv["legacy_balance_sats"] = legacy_balance_sats
|
||||
rv["legacy_balance"] = ci.format_amount(legacy_balance_sats)
|
||||
rv["message"] = "Legacy balance too low to transfer"
|
||||
if has_non_bip84_funds:
|
||||
rv["legacy_balance_sats"] = non_bip84_balance_sats
|
||||
rv["legacy_balance"] = ci.format_amount(non_bip84_balance_sats)
|
||||
rv["message"] = "Non-derivable balance too low to transfer"
|
||||
else:
|
||||
rv["legacy_balance_sats"] = 0
|
||||
rv["legacy_balance"] = "0"
|
||||
rv["message"] = "All funds on native segwit addresses"
|
||||
rv["message"] = "All funds on BIP84 addresses"
|
||||
else:
|
||||
rv["show_transfer_option"] = can_transfer
|
||||
if balance_sats == 0:
|
||||
|
||||
+24
-5
@@ -152,15 +152,17 @@ class Jsonrpc:
|
||||
pass
|
||||
|
||||
|
||||
def callrpc(rpc_port, auth, method, params=[], wallet=None, host="127.0.0.1"):
|
||||
def callrpc(
|
||||
rpc_port, auth, method, params=[], wallet=None, host="127.0.0.1", timeout=None
|
||||
):
|
||||
if _use_rpc_pooling:
|
||||
return callrpc_pooled(rpc_port, auth, method, params, wallet, host)
|
||||
return callrpc_pooled(rpc_port, auth, method, params, wallet, host, timeout)
|
||||
|
||||
try:
|
||||
url = "http://{}@{}:{}/".format(auth, host, rpc_port)
|
||||
if wallet is not None:
|
||||
url += "wallet/" + urllib.parse.quote(wallet)
|
||||
x = Jsonrpc(url)
|
||||
x = Jsonrpc(url, timeout=timeout if timeout else 10)
|
||||
|
||||
v = x.json_request(method, params)
|
||||
x.close()
|
||||
@@ -174,7 +176,9 @@ def callrpc(rpc_port, auth, method, params=[], wallet=None, host="127.0.0.1"):
|
||||
return r["result"]
|
||||
|
||||
|
||||
def callrpc_pooled(rpc_port, auth, method, params=[], wallet=None, host="127.0.0.1"):
|
||||
def callrpc_pooled(
|
||||
rpc_port, auth, method, params=[], wallet=None, host="127.0.0.1", timeout=None
|
||||
):
|
||||
from .rpc_pool import get_rpc_pool
|
||||
import http.client
|
||||
import socket
|
||||
@@ -183,6 +187,20 @@ def callrpc_pooled(rpc_port, auth, method, params=[], wallet=None, host="127.0.0
|
||||
if wallet is not None:
|
||||
url += "wallet/" + urllib.parse.quote(wallet)
|
||||
|
||||
if timeout:
|
||||
try:
|
||||
conn = Jsonrpc(url, timeout=timeout)
|
||||
v = conn.json_request(method, params)
|
||||
r = json.loads(v.decode("utf-8"))
|
||||
conn.close()
|
||||
if "error" in r and r["error"] is not None:
|
||||
raise ValueError("RPC error " + str(r["error"]))
|
||||
return r["result"]
|
||||
except ValueError:
|
||||
raise
|
||||
except Exception as ex:
|
||||
raise ValueError(f"RPC server error: {ex}, method: {method}")
|
||||
|
||||
max_connections = _rpc_pool_settings.get("max_connections_per_daemon", 5)
|
||||
pool = get_rpc_pool(url, max_connections)
|
||||
|
||||
@@ -247,7 +265,7 @@ def make_rpc_func(port, auth, wallet=None, host="127.0.0.1"):
|
||||
wallet = wallet
|
||||
host = host
|
||||
|
||||
def rpc_func(method, params=None, wallet_override=None):
|
||||
def rpc_func(method, params=None, wallet_override=None, timeout=None):
|
||||
return callrpc(
|
||||
port,
|
||||
auth,
|
||||
@@ -255,6 +273,7 @@ def make_rpc_func(port, auth, wallet=None, host="127.0.0.1"):
|
||||
params,
|
||||
wallet if wallet_override is None else wallet_override,
|
||||
host,
|
||||
timeout=timeout,
|
||||
)
|
||||
|
||||
return rpc_func
|
||||
|
||||
@@ -610,7 +610,7 @@ function ensureToastContainer() {
|
||||
clickAction = `onclick="window.location.href='/bid/${options.bidId}'"`;
|
||||
cursorStyle = 'cursor-pointer';
|
||||
} else if (options.coinSymbol) {
|
||||
clickAction = `onclick="window.location.href='/wallet/${options.coinSymbol}'"`;
|
||||
clickAction = `onclick="window.location.href='/wallet/${options.coinSymbol.toLowerCase()}'"`;
|
||||
cursorStyle = 'cursor-pointer';
|
||||
} else if (options.releaseUrl) {
|
||||
clickAction = `onclick="window.open('${options.releaseUrl}', '_blank')"`;
|
||||
@@ -739,9 +739,10 @@ function ensureToastContainer() {
|
||||
case 'sweep_completed':
|
||||
const sweepAmount = parseFloat(data.amount || 0).toFixed(8).replace(/\.?0+$/, '');
|
||||
const sweepFee = parseFloat(data.fee || 0).toFixed(8).replace(/\.?0+$/, '');
|
||||
toastTitle = `Swept ${sweepAmount} ${data.coin_name} to RPC wallet`;
|
||||
toastOptions.subtitle = `Fee: ${sweepFee} ${data.coin_name} • TXID: ${(data.txid || '').substring(0, 12)}...`;
|
||||
toastOptions.coinSymbol = data.coin_name;
|
||||
const sweepTicker = data.ticker || data.coin_name;
|
||||
toastTitle = `Swept ${sweepAmount} ${sweepTicker} to RPC wallet`;
|
||||
toastOptions.subtitle = `Fee: ${sweepFee} ${sweepTicker} • TXID: ${(data.txid || '').substring(0, 12)}...`;
|
||||
toastOptions.coinSymbol = sweepTicker;
|
||||
toastOptions.txid = data.txid;
|
||||
toastType = 'sweep_completed';
|
||||
shouldShowToast = true;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const PAGE_SIZE = 50;
|
||||
|
||||
const state = {
|
||||
dentities: new Map(),
|
||||
identities: new Map(),
|
||||
currentPage: 1,
|
||||
wsConnected: false,
|
||||
jsonData: [],
|
||||
|
||||
@@ -251,15 +251,15 @@
|
||||
let transferSection = '';
|
||||
if (info.require_transfer && info.legacy_balance_sats > 0) {
|
||||
transferSection = `
|
||||
<div class="bg-yellow-100 dark:bg-yellow-900/50 border border-yellow-400 dark:border-yellow-600 rounded-lg p-3 mb-3">
|
||||
<p class="text-sm font-medium text-yellow-800 dark:text-yellow-200 mb-2">Legacy Funds Transfer Required</p>
|
||||
<p class="text-xs text-gray-700 dark:text-gray-300 mb-2">
|
||||
<strong>${info.legacy_balance} ${info.coin}</strong> on legacy addresses will be automatically transferred to a native segwit address.
|
||||
<div class="bg-gray-100 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">Funds Transfer Required</p>
|
||||
<p class="text-xs text-gray-700 dark:text-gray-200 mb-2">
|
||||
<strong>${info.legacy_balance} ${info.coin}</strong> on non-derivable addresses will be automatically transferred to a BIP84 address.
|
||||
</p>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400 mb-2">
|
||||
<p class="text-xs text-gray-600 dark:text-gray-300 mb-2">
|
||||
Est. fee: ${info.estimated_fee} ${info.coin}
|
||||
</p>
|
||||
<p class="text-xs text-gray-700 dark:text-gray-300">
|
||||
<p class="text-xs text-gray-700 dark:text-gray-200">
|
||||
This ensures your funds are recoverable using the extended key backup in external Electrum wallets.
|
||||
</p>
|
||||
<input type="hidden" name="transfer_choice" value="auto">
|
||||
@@ -267,8 +267,8 @@
|
||||
`;
|
||||
} 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 class="text-gray-700 dark:text-gray-300 text-xs mb-3">
|
||||
Some funds on non-derivable addresses (${info.legacy_balance} ${info.coin}) - too low to transfer.
|
||||
</p>
|
||||
`;
|
||||
}
|
||||
@@ -280,11 +280,22 @@
|
||||
</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>
|
||||
<code id="extendedKeyDisplay" class="text-xs break-all font-mono text-gray-900 dark:text-gray-100">${'*'.repeat(Math.min(data.account_key.length, 80))}</code>
|
||||
<code id="extendedKeyActual" class="text-xs break-all select-all font-mono text-gray-900 dark:text-gray-100 hidden">${data.account_key}</code>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<button type="button" id="toggleKeyVisibility" class="px-3 py-1 text-xs bg-blue-500 hover:bg-blue-600 text-white rounded">
|
||||
Show Key
|
||||
</button>
|
||||
</div>
|
||||
<div class="text-xs text-gray-600 dark:text-gray-300 mb-3 bg-gray-100 dark:bg-gray-600 border border-gray-300 dark:border-gray-500 rounded p-2">
|
||||
<p class="font-medium mb-1 text-gray-800 dark:text-gray-100">To import in Electrum wallet:</p>
|
||||
<ol class="list-decimal list-inside space-y-0.5">
|
||||
<li>Open Electrum → File → New/Restore</li>
|
||||
<li>Choose "Standard wallet" → "Use a master key"</li>
|
||||
<li>Paste this key (starts with zprv... or yprv...)</li>
|
||||
</ol>
|
||||
</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">
|
||||
@@ -294,6 +305,23 @@
|
||||
</div>
|
||||
`;
|
||||
|
||||
const toggleBtn = document.getElementById('toggleKeyVisibility');
|
||||
const keyDisplay = document.getElementById('extendedKeyDisplay');
|
||||
const keyActual = document.getElementById('extendedKeyActual');
|
||||
if (toggleBtn && keyDisplay && keyActual) {
|
||||
toggleBtn.addEventListener('click', () => {
|
||||
if (keyDisplay.classList.contains('hidden')) {
|
||||
keyDisplay.classList.remove('hidden');
|
||||
keyActual.classList.add('hidden');
|
||||
toggleBtn.textContent = 'Show Key';
|
||||
} else {
|
||||
keyDisplay.classList.add('hidden');
|
||||
keyActual.classList.remove('hidden');
|
||||
toggleBtn.textContent = 'Hide Key';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const checkbox = document.getElementById('walletModeKeyConfirmCheckbox');
|
||||
if (checkbox) {
|
||||
checkbox.addEventListener('change', () => this.updateConfirmButtonState());
|
||||
@@ -362,21 +390,21 @@
|
||||
</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">
|
||||
<input type="radio" name="transfer_choice" value="auto" 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">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">
|
||||
<input type="radio" name="transfer_choice" value="manual" 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">
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400 mt-3">
|
||||
If you skip transfer, you will need to manually send funds from lite wallet addresses to your RPC wallet.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -87,7 +87,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
if (coinData.scan_status) {
|
||||
if (coinData.scan_status || coinData.electrum_synced !== undefined) {
|
||||
this.updateScanStatus(coinData);
|
||||
}
|
||||
|
||||
@@ -110,7 +110,7 @@
|
||||
if (!scanStatusEl) return;
|
||||
|
||||
const status = coinData.scan_status;
|
||||
if (status.in_progress) {
|
||||
if (status && status.in_progress) {
|
||||
scanStatusEl.innerHTML = `
|
||||
<div class="flex items-center justify-between text-xs">
|
||||
<span class="text-blue-600 dark:text-blue-300">
|
||||
@@ -126,13 +126,29 @@
|
||||
<div class="bg-blue-600 dark:bg-blue-400 h-1 rounded-full transition-all" style="width: ${status.progress}%"></div>
|
||||
</div>
|
||||
`;
|
||||
} else if (coinData.electrum_synced) {
|
||||
const height = coinData.electrum_height || '';
|
||||
scanStatusEl.innerHTML = `
|
||||
<div class="bg-green-50 dark:bg-gray-500 p-2 rounded">
|
||||
<div class="flex items-center text-xs text-green-600 dark:text-green-400">
|
||||
Electrum Wallet Synced (${height})
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
} else if (coinData.electrum_synced === false) {
|
||||
scanStatusEl.innerHTML = `
|
||||
<div class="bg-yellow-50 dark:bg-gray-500 p-2 rounded">
|
||||
<div class="flex items-center text-xs text-yellow-600 dark:text-yellow-400">
|
||||
Waiting for Electrum Server...
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
scanStatusEl.innerHTML = `
|
||||
<div class="flex items-center text-xs text-green-600 dark:text-green-400">
|
||||
<svg class="inline-block w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
||||
</svg>
|
||||
Electrum Wallet Synced
|
||||
<div class="bg-green-50 dark:bg-gray-500 p-2 rounded">
|
||||
<div class="flex items-center text-xs text-green-600 dark:text-green-400">
|
||||
Electrum Wallet Synced
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -167,29 +167,31 @@
|
||||
<div class="bg-gray-50 dark:bg-gray-700 rounded-lg p-4 space-y-4">
|
||||
|
||||
<div>
|
||||
<p class="text-xs font-medium text-gray-900 dark:text-white mb-1">To Light Wallet:</p>
|
||||
<p class="text-xs font-medium text-gray-900 dark:text-white mb-1">Light Wallet Mode (Electrum):</p>
|
||||
<ul class="text-xs text-gray-700 dark:text-gray-200 space-y-0.5 ml-3">
|
||||
<li>• Your full node stops running</li>
|
||||
<li>• Light wallet uses your seed to access existing funds</li>
|
||||
<li>• No transfer needed - same seed, same funds</li>
|
||||
<li>• No blockchain download needed - connect via external Electrum servers</li>
|
||||
<li>• Uses BIP84 derivation (native SegWit) - lower fees, modern addresses (bc1q.../ltc1q...)</li>
|
||||
<li>• You receive an extended private key (zprv/...) that can be imported into external wallets</li>
|
||||
<li>• Best for: fresh installs, low storage, quick setup, mobile-friendly</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-xs font-medium text-gray-900 dark:text-white mb-1">While in Light Wallet mode:</p>
|
||||
<p class="text-xs font-medium text-gray-900 dark:text-white mb-1">Full Node Mode (RPC):</p>
|
||||
<ul class="text-xs text-gray-700 dark:text-gray-200 space-y-0.5 ml-3">
|
||||
<li>• Light wallet generates NEW addresses (BIP84 format: bc1q.../ltc1q...)</li>
|
||||
<li>• Any funds you RECEIVE go to these new addresses</li>
|
||||
<li>• Your full node doesn't know about these addresses</li>
|
||||
<li>• Maximum privacy - no external servers, your node validates everything</li>
|
||||
<li>• More wallet features: coin control, RBF, CPFP, raw transactions</li>
|
||||
<li>• Supports legacy address types and coin-specific features (e.g. MWEB for LTC)</li>
|
||||
<li>• Best for: existing node users, power users, maximum control</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-xs font-medium text-gray-900 dark:text-white mb-1">To Full Node:</p>
|
||||
<p class="text-xs font-medium text-gray-900 dark:text-white mb-1">When switching modes:</p>
|
||||
<ul class="text-xs text-gray-700 dark:text-gray-200 space-y-0.5 ml-3">
|
||||
<li>• Full node can't see funds on light wallet addresses</li>
|
||||
<li>• These funds must be SENT back to your node wallet (real transaction, network fee applies based on current rate)</li>
|
||||
<li>• Enable "Auto-transfer" in Fund Transfer section to do this automatically on unlock</li>
|
||||
<li>• To Light: Save your BIP84 key shown during switch (for external wallet import)</li>
|
||||
<li>• To Full Node: Funds on light wallet addresses must be transferred (network fee applies)</li>
|
||||
<li>• Both modes share the same seed - switching is safe, just save keys when shown</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -259,13 +261,12 @@
|
||||
</div>
|
||||
|
||||
<div id="fund-transfer-section-{{ c.name }}" class="mb-6 hidden">
|
||||
{% if c.lite_wallet_balance %}
|
||||
<h4 class="text-sm font-medium text-gray-900 dark:text-white mb-4">
|
||||
Fund Transfer
|
||||
Pending Balance
|
||||
</h4>
|
||||
|
||||
<div class="bg-gray-50 dark:bg-gray-700 rounded-lg p-4">
|
||||
{% if c.lite_wallet_balance %}
|
||||
<div class="mb-4 p-3 bg-orange-50 dark:bg-orange-900/30 border border-orange-200 dark:border-orange-700 rounded-lg">
|
||||
<div class="p-3 bg-orange-50 dark:bg-orange-900/30 border border-orange-200 dark:border-orange-700 rounded-lg">
|
||||
<p class="text-sm font-medium text-orange-800 dark:text-orange-200 mb-2">
|
||||
<svg class="inline w-4 h-4 mr-1" 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"></path></svg>
|
||||
Light Wallet Balance Detected
|
||||
@@ -292,24 +293,24 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<label class="flex items-center cursor-pointer">
|
||||
<input type="checkbox" name="auto_transfer_{{ c.name }}" value="true" {% if c.auto_transfer_on_mode_switch != false %}checked{% endif %} class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
|
||||
<span class="ml-2 text-sm font-medium text-red-600 dark:text-red-400">Auto-transfer funds when switching to Full Node</span>
|
||||
</label>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mt-2 ml-6">Funds in light wallet addresses will be swept to your RPC wallet after switching. Network fee applies based on current rate.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if general_settings.debug %}
|
||||
<div class="mt-4 pt-4 border-t border-gray-200 dark:border-gray-600">
|
||||
{% if general_settings.debug %}
|
||||
<h4 class="text-sm font-medium text-gray-900 dark:text-white mb-4 {% if c.lite_wallet_balance %}mt-6{% endif %}">
|
||||
Advanced
|
||||
</h4>
|
||||
<div class="bg-gray-50 dark:bg-gray-700 rounded-lg p-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Address Gap Limit</label>
|
||||
<div class="flex items-center">
|
||||
<input type="number" name="gap_limit_{{ c.name }}" value="{{ c.address_gap_limit }}" min="5" max="100" class="hover:border-blue-500 bg-gray-50 text-gray-900 appearance-none dark:bg-gray-700 dark:text-white border border-gray-300 dark:border-gray-600 dark:placeholder-gray-400 text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-24 p-2.5 focus:ring-1 focus:outline-none" placeholder="20">
|
||||
<input type="number" name="gap_limit_{{ c.name }}" value="{{ c.address_gap_limit }}" min="5" max="100" class="hover:border-blue-500 bg-gray-50 text-gray-900 appearance-none dark:bg-gray-700 dark:text-white border border-gray-300 dark:border-gray-600 dark:placeholder-gray-400 text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-24 p-2.5 focus:ring-1 focus:outline-none" placeholder="50">
|
||||
<span class="ml-2 text-xs text-gray-500 dark:text-gray-400">(5-100)</span>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Number of consecutive unfunded addresses to scan. Increase if you generated many unused addresses.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
@@ -864,7 +865,7 @@
|
||||
Switch Mode
|
||||
</button>
|
||||
<button type="button" id="walletModeCancel"
|
||||
class="px-4 py-2.5 bg-gray-300 hover:bg-gray-400 dark:bg-gray-600 dark:hover:bg-gray-700 font-medium text-sm text-gray-700 dark:text-white rounded-md focus:ring-0 focus:outline-none">
|
||||
class="px-4 py-2.5 bg-gray-200 hover:bg-gray-300 dark:bg-gray-600 dark:hover:bg-gray-700 font-medium text-sm text-gray-800 dark:text-white rounded-md border border-gray-300 dark:border-gray-500 focus:ring-0 focus:outline-none">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
@@ -875,4 +876,4 @@
|
||||
|
||||
<script src="/static/js/pages/settings-page.js"></script>
|
||||
|
||||
{% include 'footer.html' %}
|
||||
{% include 'footer.html' %}
|
||||
@@ -35,6 +35,8 @@
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
|
||||
|
||||
{% if w.havedata %}
|
||||
{% if w.error %}
|
||||
<section class="py-4 px-6" id="messages_error" role="alert">
|
||||
@@ -183,8 +185,20 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% elif w.cid == '13' %} {# FIRO #}
|
||||
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
|
||||
<td class="py-3 px-6 bold"> <span class="inline-flex align-middle items-center justify-center w-9 h-10 bg-white-50 rounded"> <img class="h-7" src="/static/images/coins/{{ w.name }}.png" alt="{{ w.name }} Spark"> </span>Spark Balance: </td>
|
||||
<td class="py-3 px-6 bold">
|
||||
<span class="coinname-value" data-coinname="{{ w.name }}">{{ w.spark_balance }} {{ w.ticker }}</span>
|
||||
(<span class="usd-value"></span>)
|
||||
{% if w.spark_pending %}
|
||||
<span class="inline-block py-1 px-2 rounded-full bg-green-100 text-green-500 dark:bg-gray-500 dark:text-green-500">Pending: +{{ w.spark_pending }} {{ w.ticker }} </span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{# / LTC #}
|
||||
{# / FIRO #}
|
||||
{% if w.locked_utxos %}
|
||||
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
|
||||
<td class="py-3 px-6 bold">Locked Outputs:</td>
|
||||
@@ -230,7 +244,11 @@
|
||||
<td class="py-3 px-6 bold">Synced:</td>
|
||||
<td class="py-3 px-6">
|
||||
{% if is_electrum_mode %}
|
||||
<span class="text-green-600 dark:text-green-400">Electrum Wallet Synced</span>
|
||||
{% if w.electrum_synced %}
|
||||
<span class="text-green-600 dark:text-green-400">Electrum Wallet Synced ({{ w.electrum_height }})</span>
|
||||
{% else %}
|
||||
<span class="text-yellow-600 dark:text-yellow-400">Waiting for Electrum Server...</span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{{ w.synced }}
|
||||
{% endif %}
|
||||
@@ -394,8 +412,8 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if w.cid in '1, 3, 6, 9' %}
|
||||
{# PART | LTC | XMR | WOW | #}
|
||||
{% if w.cid in '1, 3, 6, 9, 13' %}
|
||||
{# PART | LTC | XMR | WOW | FIRO #}
|
||||
<div class="w-full md:w-1/2 p-3 flex justify-center items-center">
|
||||
<div class="h-full">
|
||||
<div class="flex flex-wrap -m-3">
|
||||
@@ -444,6 +462,22 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
{# / LTC #}
|
||||
{% elif w.cid == '13' %}
|
||||
{# FIRO #}
|
||||
<div id="qrcode-spark" class="qrcode" data-qrcode data-address="{{ w.spark_address }}"> </div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="font-normal bold text-gray-500 text-center dark:text-white mb-5">Spark Address: </div>
|
||||
<div class="text-center relative">
|
||||
<div class="input-like-container hover:border-blue-500 bg-gray-50 text-gray-900 appearance-none pr-10 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-400 text-lg lg:text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-0" id="stealth_address">{{ w.spark_address }}</div>
|
||||
<span class="absolute inset-y-0 right-0 flex items-center pr-3 cursor-pointer" id="copyIcon"></span>
|
||||
</div>
|
||||
<div class="opacity-100 text-gray-500 dark:text-gray-100 flex justify-center items-center">
|
||||
<div class="py-3 px-6 bold mt-5">
|
||||
<button type="submit" class="flex justify-center py-2 px-4 bg-blue-500 hover:bg-blue-600 font-medium text-sm text-white border border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none" name="newsparkaddr_{{ w.cid }}" value="New Spark Address"> {{ circular_arrows_svg }} New Spark Address </button>
|
||||
</div>
|
||||
</div>
|
||||
{# / FIRO #}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -507,6 +541,15 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% elif w.cid == '13' %}
|
||||
{# FIRO #}
|
||||
<tr class="opacity-100 text-gray-500 dark:text-gray-100">
|
||||
<td class="py-4 pl-6 bold w-1/4"> <span class="inline-flex align-middle items-center justify-center w-9 h-10 bg-white-50 rounded"> <img class="h-7" src="/static/images/coins/{{ w.name }}.png" alt="{{ w.name }}"> </span>Spark Balance: </td>
|
||||
<td class="py-3 px-6">
|
||||
<span class="coinname-value" data-coinname="{{ w.name }}">{{ w.spark_balance }} {{ w.ticker }}</span>
|
||||
(<span class="usd-value"></span>)
|
||||
</td>
|
||||
</tr>
|
||||
{% elif w.cid == '1' %}
|
||||
{# PART #}
|
||||
<tr class="opacity-100 text-gray-500 dark:text-gray-100">
|
||||
@@ -597,6 +640,14 @@
|
||||
<button type="button" class="ml-2 py-1 px-2 bg-blue-500 text-white text-lg lg:text-sm rounded-md focus:outline-none" onclick="setAmount(1, '{{ w.balance }}', {{ w.cid }}, '{{ w.mweb_balance }}')">100%</button>
|
||||
|
||||
{# / LTC #}
|
||||
|
||||
{% elif w.cid == '13' %}
|
||||
{# FIRO #}
|
||||
<button type="button" class="hidden md:block py-1 px-2 bg-blue-500 text-white text-lg lg:text-sm rounded-md focus:outline-none" onclick="setAmount(0.25, '{{ w.balance }}', {{ w.cid }}, '{{ w.spark_balance }}')">25%</button>
|
||||
<button type="button" class="hidden md:block ml-2 py-1 px-2 bg-blue-500 text-white text-lg lg:text-sm rounded-md focus:outline-none" onclick="setAmount(0.5, '{{ w.balance }}', {{ w.cid }}, '{{ w.spark_balance }}')">50%</button>
|
||||
<button type="button" class="ml-2 py-1 px-2 bg-blue-500 text-white text-lg lg:text-sm rounded-md focus:outline-none" onclick="setAmount(1, '{{ w.balance }}', {{ w.cid }}, '{{ w.spark_balance }}')">100%</button>
|
||||
|
||||
{# / FIRO #}
|
||||
{% else %}
|
||||
<button type="button" class="hidden md:block py-1 px-2 bg-blue-500 text-white text-lg lg:text-sm rounded-md focus:outline-none" onclick="setAmount(0.25, '{{ w.balance }}', {{ w.cid }})">25%</button>
|
||||
<button type="button" class="hidden md:block ml-2 py-1 px-2 bg-blue-500 text-white text-lg lg:text-sm rounded-md focus:outline-none" onclick="setAmount(0.5, '{{ w.balance }}', {{ w.cid }})">50%</button>
|
||||
@@ -663,8 +714,21 @@
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% elif w.cid == '13' %} {# FIRO #}
|
||||
<tr class="opacity-100 text-gray-500 dark:text-gray-100">
|
||||
<td class="py-3 px-6 bold">Type From:</td>
|
||||
<td class="py-3 px-6">
|
||||
<div class="w-full md:flex-1">
|
||||
<div class="relative"> {{ select_box_arrow_svg }} <select id="withdraw_type" class="{{ select_box_class }}" name="withdraw_type_from_{{ w.cid }}">
|
||||
<option value="spark" {% if w.wd_type_from=='spark' %} selected{% endif %}>Spark</option>
|
||||
<option value="plain" {% if w.wd_type_from=='plain' %} selected{% endif %}>Plain</option>
|
||||
</select> </div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{# / LTC #}
|
||||
{# / FIRO #}
|
||||
{% if w.cid not in '6,9' %} {# Not XMR WOW #}
|
||||
<tr class="opacity-100 text-gray-500 dark:text-gray-100">
|
||||
<td class="py-3 px-6 bold">Fee Rate:</td>
|
||||
|
||||
@@ -148,6 +148,28 @@
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{# / LTC #}
|
||||
{% if w.cid == '13' %} {# FIRO #}
|
||||
<div class="flex mb-2 justify-between items-center">
|
||||
<h4 class="text-xs font-medium dark:text-white">Spark Balance:</h4>
|
||||
<span class="bold inline-block py-1 px-2 rounded-full bg-blue-100 text-xs text-black-500 dark:bg-gray-500 dark:text-gray-200 coinname-value" data-coinname="{{ w.name }}">{{ w.spark_balance }} {{ w.ticker }}</span>
|
||||
</div>
|
||||
<div class="flex mb-2 justify-between items-center">
|
||||
<h4 class="text-xs font-medium dark:text-white">Spark USD value:</h4>
|
||||
<div class="bold inline-block py-1 px-2 rounded-full bg-blue-100 text-xs text-black-500 dark:bg-gray-500 dark:text-gray-200 usd-value"></div>
|
||||
</div>
|
||||
{% if w.spark_pending %}
|
||||
<div class="flex mb-2 justify-between items-center">
|
||||
<h4 class="text-xs font-bold text-green-500 dark:text-green-500">Spark Pending:</h4>
|
||||
<span class="bold inline-block py-1 px-2 rounded-full bg-green-100 text-xs text-green-500 dark:bg-gray-500 dark:text-green-500 coinname-value" data-coinname="{{ w.name }}">
|
||||
+{{ w.spark_pending }} {{ w.ticker }}</span>
|
||||
</div>
|
||||
<div class="flex mb-2 justify-between items-center">
|
||||
<h4 class="text-xs font-bold text-green-500 dark:text-green-500">Spark Pending USD value:</h4>
|
||||
<div class="bold inline-block py-1 px-2 rounded-full bg-green-100 text-xs text-green-500 dark:bg-gray-500 dark:text-green-500 usd-value"></div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{# / FIRO #}
|
||||
<hr class="border-t border-gray-100 dark:border-gray-500 my-5">
|
||||
<div class="flex mb-2 justify-between items-center">
|
||||
<h4 class="text-xs font-medium dark:text-white">Blocks:</h4>
|
||||
@@ -193,10 +215,16 @@
|
||||
<div class="bg-blue-600 dark:bg-blue-400 h-1 rounded-full" style="width: {{ w.scan_status.progress }}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
{% elif w.electrum_synced %}
|
||||
<div class="bg-green-50 dark:bg-gray-500 p-2 rounded">
|
||||
<div class="flex items-center text-xs text-green-600 dark:text-green-400">
|
||||
Electrum Wallet Synced
|
||||
Electrum Wallet Synced ({{ w.electrum_height }})
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="bg-yellow-50 dark:bg-gray-500 p-2 rounded">
|
||||
<div class="flex items-center text-xs text-yellow-600 dark:text-yellow-400">
|
||||
Waiting for Electrum Server...
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -189,7 +189,7 @@ def page_settings(self, url_split, post_string):
|
||||
)
|
||||
data["auto_transfer_now"] = transfer_value == "true"
|
||||
gap_limit_str = get_data_entry_or(
|
||||
form_data, "gap_limit_" + name, "20"
|
||||
form_data, "gap_limit_" + name, "50"
|
||||
).strip()
|
||||
try:
|
||||
gap_limit = int(gap_limit_str)
|
||||
@@ -295,7 +295,7 @@ def page_settings(self, url_split, post_string):
|
||||
"supports_electrum": name in electrum_supported_coins,
|
||||
"clearnet_servers_text": clearnet_text,
|
||||
"onion_servers_text": onion_text,
|
||||
"address_gap_limit": c.get("address_gap_limit", 20),
|
||||
"address_gap_limit": c.get("address_gap_limit", 50),
|
||||
}
|
||||
)
|
||||
if name in ("monero", "wownero"):
|
||||
|
||||
@@ -90,6 +90,10 @@ def format_wallet_data(swap_client, ci, w):
|
||||
wf["mweb_address"] = w.get("mweb_address", "?")
|
||||
wf["mweb_balance"] = w.get("mweb_balance", "?")
|
||||
wf["mweb_pending"] = w.get("mweb_pending", "?")
|
||||
elif ci.coin_type() == Coins.FIRO:
|
||||
wf["spark_address"] = w.get("spark_address", "?")
|
||||
wf["spark_balance"] = w.get("spark_balance", "?")
|
||||
wf["spark_pending"] = w.get("spark_pending", "?")
|
||||
|
||||
if hasattr(ci, "getScanStatus"):
|
||||
wf["scan_status"] = ci.getScanStatus()
|
||||
@@ -114,6 +118,13 @@ def format_wallet_data(swap_client, ci, w):
|
||||
except Exception:
|
||||
wf["electrum_connected"] = False
|
||||
wf["electrum_status"] = "error"
|
||||
try:
|
||||
sync_status = backend.getSyncStatus()
|
||||
wf["electrum_synced"] = sync_status.get("synced", False)
|
||||
wf["electrum_height"] = sync_status.get("height", 0)
|
||||
except Exception:
|
||||
wf["electrum_synced"] = False
|
||||
wf["electrum_height"] = 0
|
||||
|
||||
checkAddressesOwned(swap_client, ci, wf)
|
||||
return wf
|
||||
@@ -264,6 +275,8 @@ def page_wallet(self, url_split, post_string):
|
||||
force_refresh = True
|
||||
elif have_data_entry(form_data, "newmwebaddr_" + cid):
|
||||
swap_client.cacheNewStealthAddressForCoin(coin_id)
|
||||
elif have_data_entry(form_data, "newsparkaddr_" + cid):
|
||||
swap_client.cacheNewStealthAddressForCoin(coin_id)
|
||||
elif have_data_entry(form_data, "reseed_" + cid):
|
||||
try:
|
||||
swap_client.reseedWallet(coin_id)
|
||||
@@ -325,7 +338,7 @@ def page_wallet(self, url_split, post_string):
|
||||
page_data["wd_type_to_" + cid] = type_to
|
||||
except Exception as e: # noqa: F841
|
||||
err_messages.append("Missing type")
|
||||
elif coin_id == Coins.LTC:
|
||||
elif coin_id in (Coins.LTC, Coins.FIRO):
|
||||
try:
|
||||
type_from = form_data[bytes("withdraw_type_from_" + cid, "utf-8")][
|
||||
0
|
||||
@@ -354,9 +367,9 @@ def page_wallet(self, url_split, post_string):
|
||||
value, ticker, type_from, type_to, address, txid
|
||||
)
|
||||
)
|
||||
elif coin_id == Coins.LTC:
|
||||
txid = swap_client.withdrawLTC(
|
||||
type_from, value, address, subfee
|
||||
elif coin_id in (Coins.LTC, Coins.FIRO):
|
||||
txid = swap_client.withdrawCoinExtended(
|
||||
coin_id, type_from, value, address, subfee
|
||||
)
|
||||
messages.append(
|
||||
"Withdrew {} {} (from {}) to address {}<br/>In txid: {}".format(
|
||||
@@ -429,8 +442,12 @@ def page_wallet(self, url_split, post_string):
|
||||
if swap_client.debug is True:
|
||||
swap_client.log.error(traceback.format_exc())
|
||||
|
||||
is_electrum_mode = (
|
||||
swap_client.coin_clients.get(coin_id, {}).get("connection_type") == "electrum"
|
||||
)
|
||||
|
||||
swap_client.updateWalletsInfo(
|
||||
force_refresh, only_coin=coin_id, wait_for_complete=True
|
||||
force_refresh, only_coin=coin_id, wait_for_complete=not is_electrum_mode
|
||||
)
|
||||
wallets = swap_client.getCachedWalletsInfo({"coin_id": coin_id})
|
||||
wallet_data = {}
|
||||
@@ -469,6 +486,8 @@ def page_wallet(self, url_split, post_string):
|
||||
wallet_data["main_address"] = w.get("main_address", "Refresh necessary")
|
||||
elif k == Coins.LTC:
|
||||
wallet_data["mweb_address"] = w.get("mweb_address", "Refresh necessary")
|
||||
elif k == Coins.FIRO:
|
||||
wallet_data["spark_address"] = w.get("spark_address", "Refresh necessary")
|
||||
|
||||
if "wd_type_from_" + cid in page_data:
|
||||
wallet_data["wd_type_from"] = page_data["wd_type_from_" + cid]
|
||||
|
||||
@@ -197,7 +197,11 @@ class ElectrumBackend(WalletBackend):
|
||||
self._cached_height_time = 0
|
||||
self._height_cache_ttl = 5
|
||||
|
||||
self._max_batch_size = 10
|
||||
self._cached_fee = {}
|
||||
self._cached_fee_time = {}
|
||||
self._fee_cache_ttl = 300
|
||||
|
||||
self._max_batch_size = 5
|
||||
self._background_mode = False
|
||||
|
||||
def setBackgroundMode(self, enabled: bool):
|
||||
@@ -206,13 +210,20 @@ class ElectrumBackend(WalletBackend):
|
||||
def _call(self, method: str, params: list = None, timeout: int = 10):
|
||||
if self._background_mode and hasattr(self._server, "call_background"):
|
||||
return self._server.call_background(method, params, timeout)
|
||||
if hasattr(self._server, "call_user"):
|
||||
return self._server.call_user(method, params, timeout)
|
||||
return self._server.call(method, params, timeout)
|
||||
|
||||
def _call_batch(self, calls: list, timeout: int = 15):
|
||||
if self._background_mode and hasattr(self._server, "call_batch_background"):
|
||||
return self._server.call_batch_background(calls, timeout)
|
||||
if hasattr(self._server, "call_batch_user"):
|
||||
return self._server.call_batch_user(calls, timeout)
|
||||
return self._server.call_batch(calls, timeout)
|
||||
|
||||
def _is_server_stopping(self) -> bool:
|
||||
return getattr(self._server, "_stopping", False)
|
||||
|
||||
def _split_batch_call(
|
||||
self, scripthashes: list, method: str, batch_size: int = None
|
||||
) -> list:
|
||||
@@ -221,19 +232,30 @@ class ElectrumBackend(WalletBackend):
|
||||
|
||||
all_results = []
|
||||
for i in range(0, len(scripthashes), batch_size):
|
||||
if self._is_server_stopping():
|
||||
self._log.debug("_split_batch_call: server stopping, aborting")
|
||||
break
|
||||
chunk = scripthashes[i : i + batch_size]
|
||||
try:
|
||||
calls = [(method, [sh]) for sh in chunk]
|
||||
results = self._call_batch(calls)
|
||||
all_results.extend(results)
|
||||
except Exception as e:
|
||||
self._log.debug(f"Batch chunk failed ({len(chunk)} items): {e}")
|
||||
except Exception:
|
||||
if self._is_server_stopping():
|
||||
self._log.debug(
|
||||
"_split_batch_call: server stopping after batch failure, aborting"
|
||||
)
|
||||
break
|
||||
for sh in chunk:
|
||||
if self._is_server_stopping():
|
||||
self._log.debug(
|
||||
"_split_batch_call: server stopping during fallback, aborting"
|
||||
)
|
||||
break
|
||||
try:
|
||||
result = self._call(method, [sh])
|
||||
all_results.append(result)
|
||||
except Exception as e2:
|
||||
self._log.debug(f"Individual call failed for {sh[:8]}...: {e2}")
|
||||
except Exception:
|
||||
all_results.append(None)
|
||||
return all_results
|
||||
|
||||
@@ -298,8 +320,10 @@ class ElectrumBackend(WalletBackend):
|
||||
if not addr_list:
|
||||
return result
|
||||
|
||||
batch_size = 10
|
||||
batch_size = self._max_batch_size
|
||||
for batch_start in range(0, len(addr_list), batch_size):
|
||||
if self._is_server_stopping():
|
||||
break
|
||||
batch = addr_list[batch_start : batch_start + batch_size]
|
||||
|
||||
addr_to_scripthash = {}
|
||||
@@ -332,6 +356,8 @@ class ElectrumBackend(WalletBackend):
|
||||
batch_success = True
|
||||
break
|
||||
except Exception as e:
|
||||
if self._is_server_stopping():
|
||||
break
|
||||
if attempt == 0:
|
||||
self._log.debug(
|
||||
f"Batch detailed balance query failed, reconnecting: {e}"
|
||||
@@ -348,6 +374,8 @@ class ElectrumBackend(WalletBackend):
|
||||
|
||||
if not batch_success:
|
||||
for addr, scripthash in addr_to_scripthash.items():
|
||||
if self._is_server_stopping():
|
||||
break
|
||||
try:
|
||||
balance = self._call(
|
||||
"blockchain.scripthash.get_balance", [scripthash]
|
||||
@@ -569,13 +597,22 @@ class ElectrumBackend(WalletBackend):
|
||||
return self._cached_height if self._cached_height > 0 else 0
|
||||
|
||||
def estimateFee(self, blocks: int = 6) -> int:
|
||||
now = time.time()
|
||||
cache_key = blocks
|
||||
if cache_key in self._cached_fee:
|
||||
if (now - self._cached_fee_time.get(cache_key, 0)) < self._fee_cache_ttl:
|
||||
return self._cached_fee[cache_key]
|
||||
|
||||
try:
|
||||
fee = self._call("blockchain.estimatefee", [blocks])
|
||||
if fee and fee > 0:
|
||||
return int(fee * 1e8 / 1000)
|
||||
return 1
|
||||
result = int(fee * 1e8 / 1000)
|
||||
self._cached_fee[cache_key] = result
|
||||
self._cached_fee_time[cache_key] = now
|
||||
return result
|
||||
return self._cached_fee.get(cache_key, 1)
|
||||
except Exception:
|
||||
return 1
|
||||
return self._cached_fee.get(cache_key, 1)
|
||||
|
||||
def isConnected(self) -> bool:
|
||||
try:
|
||||
@@ -616,6 +653,11 @@ class ElectrumBackend(WalletBackend):
|
||||
status["version"] = self.getServerVersion()
|
||||
return status
|
||||
|
||||
def recentlyReconnected(self, grace_seconds: int = 30) -> bool:
|
||||
if hasattr(self._server, "recently_reconnected"):
|
||||
return self._server.recently_reconnected(grace_seconds)
|
||||
return False
|
||||
|
||||
def getAddressHistory(self, address: str) -> List[dict]:
|
||||
if self._isUnsupportedAddress(address):
|
||||
return []
|
||||
@@ -751,5 +793,29 @@ class ElectrumBackend(WalletBackend):
|
||||
self._log.debug(f"Failed to subscribe to {address}: {e}")
|
||||
return None
|
||||
|
||||
def getSyncStatus(self) -> dict:
|
||||
import time
|
||||
|
||||
height = 0
|
||||
height_time = 0
|
||||
if hasattr(self._server, "get_subscribed_height"):
|
||||
height = self._server.get_subscribed_height()
|
||||
height_time = getattr(self._server, "_subscribed_height_time", 0)
|
||||
|
||||
if self._cached_height > 0:
|
||||
if self._cached_height > height:
|
||||
height = self._cached_height
|
||||
if self._cached_height_time > height_time:
|
||||
height_time = self._cached_height_time
|
||||
|
||||
now = time.time()
|
||||
stale_threshold = 300
|
||||
is_synced = height > 0 and (now - height_time) < stale_threshold
|
||||
return {
|
||||
"height": height,
|
||||
"synced": is_synced,
|
||||
"last_update": height_time,
|
||||
}
|
||||
|
||||
def getServer(self):
|
||||
return self._server
|
||||
|
||||
@@ -37,7 +37,7 @@ class WalletManager:
|
||||
Coins.LTC: {"mainnet": "ltc", "testnet": "tltc", "regtest": "rltc"},
|
||||
}
|
||||
|
||||
GAP_LIMIT = 20
|
||||
GAP_LIMIT = 50
|
||||
|
||||
def __init__(self, swap_client, log):
|
||||
self._gap_limits: Dict[Coins, int] = {}
|
||||
@@ -51,6 +51,12 @@ class WalletManager:
|
||||
self._migration_in_progress: set = set()
|
||||
self._balance_sync_lock = threading.Lock()
|
||||
|
||||
def getGapLimit(self, coin_type: Coins) -> int:
|
||||
return self._gap_limits.get(coin_type, self.GAP_LIMIT)
|
||||
|
||||
def setGapLimit(self, coin_type: Coins, gap_limit: int) -> None:
|
||||
self._gap_limits[coin_type] = gap_limit
|
||||
|
||||
def initialize(self, coin_type: Coins, root_key) -> None:
|
||||
if coin_type not in self.SUPPORTED_COINS:
|
||||
raise ValueError(f"Coin {coin_type} not supported by WalletManager")
|
||||
|
||||
@@ -1489,7 +1489,11 @@ class Test(BaseTest):
|
||||
v = ci.getNewRandomKey()
|
||||
s = ci.getNewRandomKey()
|
||||
S = ci.getPubkey(s)
|
||||
lock_tx_b_txid = ci.publishBLockTx(v, S, amount, fee_rate)
|
||||
result = ci.publishBLockTx(v, S, amount, fee_rate)
|
||||
if isinstance(result, tuple):
|
||||
lock_tx_b_txid, lock_tx_b_vout = result
|
||||
else:
|
||||
lock_tx_b_txid = result
|
||||
test_delay_event.wait(1)
|
||||
|
||||
addr_out = ci.getNewAddress(True)
|
||||
|
||||
@@ -412,7 +412,11 @@ class Test(TestFunctions):
|
||||
v = ci.getNewRandomKey()
|
||||
s = ci.getNewRandomKey()
|
||||
S = ci.getPubkey(s)
|
||||
lock_tx_b_txid = ci.publishBLockTx(v, S, amount, fee_rate)
|
||||
result = ci.publishBLockTx(v, S, amount, fee_rate)
|
||||
if isinstance(result, tuple):
|
||||
lock_tx_b_txid, lock_tx_b_vout = result
|
||||
else:
|
||||
lock_tx_b_txid = result
|
||||
test_delay_event.wait(1)
|
||||
|
||||
addr_out = ci.getNewAddress(False)
|
||||
|
||||
@@ -1579,7 +1579,11 @@ class BasicSwapTest(TestFunctions):
|
||||
v = ci.getNewRandomKey()
|
||||
s = ci.getNewRandomKey()
|
||||
S = ci.getPubkey(s)
|
||||
lock_tx_b_txid = ci.publishBLockTx(v, S, amount, self.test_fee_rate)
|
||||
result = ci.publishBLockTx(v, S, amount, self.test_fee_rate)
|
||||
if isinstance(result, tuple):
|
||||
lock_tx_b_txid, lock_tx_b_vout = result
|
||||
else:
|
||||
lock_tx_b_txid = result
|
||||
|
||||
addr_out = ci.getNewAddress(True)
|
||||
lock_tx_b_spend_txid = ci.spendBLockTx(
|
||||
|
||||
@@ -1218,7 +1218,11 @@ class Test(BaseTest):
|
||||
v = ci.getNewRandomKey()
|
||||
s = ci.getNewRandomKey()
|
||||
S = ci.getPubkey(s)
|
||||
lock_tx_b_txid = ci.publishBLockTx(v, S, amount, fee_rate)
|
||||
result = ci.publishBLockTx(v, S, amount, fee_rate)
|
||||
if isinstance(result, tuple):
|
||||
lock_tx_b_txid, lock_tx_b_vout = result
|
||||
else:
|
||||
lock_tx_b_txid = result
|
||||
|
||||
addr_out = ci.getNewAddress(True)
|
||||
lock_tx_b_spend_txid = ci.spendBLockTx(
|
||||
@@ -1247,7 +1251,11 @@ class Test(BaseTest):
|
||||
v = ci.getNewRandomKey()
|
||||
s = ci.getNewRandomKey()
|
||||
S = ci.getPubkey(s)
|
||||
lock_tx_b_txid = ci.publishBLockTx(v, S, amount, fee_rate)
|
||||
result = ci.publishBLockTx(v, S, amount, fee_rate)
|
||||
if isinstance(result, tuple):
|
||||
lock_tx_b_txid, lock_tx_b_vout = result
|
||||
else:
|
||||
lock_tx_b_txid = result
|
||||
|
||||
addr_out = ci.getNewAddress(True)
|
||||
for i in range(20):
|
||||
@@ -2306,7 +2314,11 @@ class Test(BaseTest):
|
||||
v = ci.getNewRandomKey()
|
||||
s = ci.getNewRandomKey()
|
||||
S = ci.getPubkey(s)
|
||||
lock_tx_b_txid = ci.publishBLockTx(v, S, amount, fee_rate)
|
||||
result = ci.publishBLockTx(v, S, amount, fee_rate)
|
||||
if isinstance(result, tuple):
|
||||
lock_tx_b_txid, lock_tx_b_vout = result
|
||||
else:
|
||||
lock_tx_b_txid = result
|
||||
|
||||
addr_out = ci.getNewStealthAddress()
|
||||
lock_tx_b_spend_txid = None
|
||||
|
||||
Reference in New Issue
Block a user