mirror of
https://github.com/basicswap/basicswap.git
synced 2026-04-09 02:47:22 +02:00
Electrum connection stability, swap fixes / UX improvements + Various fixes.
This commit is contained in:
@@ -229,23 +229,35 @@ def checkAndNotifyBalanceChange(
|
|||||||
cc["cached_balance"] = current_balance
|
cc["cached_balance"] = current_balance
|
||||||
cc["cached_total_balance"] = current_total_balance
|
cc["cached_total_balance"] = current_total_balance
|
||||||
cc["cached_unconfirmed"] = current_unconfirmed
|
cc["cached_unconfirmed"] = current_unconfirmed
|
||||||
swap_client.log.debug(
|
|
||||||
f"{ci.ticker()} balance updated (trigger: {trigger_source})"
|
suppress = False
|
||||||
)
|
if cached_balance is None or cached_total_balance is None:
|
||||||
balance_event = {
|
suppress = True
|
||||||
"event": "coin_balance_updated",
|
elif hasattr(ci, "getBackend") and ci.useBackend():
|
||||||
"coin": ci.ticker(),
|
backend = ci.getBackend()
|
||||||
"height": new_height,
|
if backend and hasattr(backend, "recentlyReconnected"):
|
||||||
"trigger": trigger_source,
|
if backend.recentlyReconnected(grace_seconds=30):
|
||||||
}
|
suppress = True
|
||||||
swap_client.ws_server.send_message_to_all(json.dumps(balance_event))
|
|
||||||
|
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:
|
except Exception as e:
|
||||||
swap_client.log.debug(
|
swap_client.log.debug(
|
||||||
f"checkAndNotifyBalanceChange {ci.ticker()}: balance check failed: {e}"
|
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):
|
def threadPollXMRChainState(swap_client, coin_type):
|
||||||
@@ -458,6 +470,7 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
|
|||||||
self._updating_wallets_info = {}
|
self._updating_wallets_info = {}
|
||||||
self._last_updated_wallets_info = 0
|
self._last_updated_wallets_info = 0
|
||||||
self._synced_addresses_from_full_node = set()
|
self._synced_addresses_from_full_node = set()
|
||||||
|
self._cached_electrum_legacy_funds = {}
|
||||||
|
|
||||||
self.check_updates_seconds = self.get_int_setting(
|
self.check_updates_seconds = self.get_int_setting(
|
||||||
"check_updates_seconds", 24 * 60 * 60, 60 * 60, 7 * 24 * 60 * 60
|
"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(
|
self.thread_pool = concurrent.futures.ThreadPoolExecutor(
|
||||||
max_workers=4, thread_name_prefix="bsp"
|
max_workers=4, thread_name_prefix="bsp"
|
||||||
)
|
)
|
||||||
|
self._electrum_spend_check_futures = {}
|
||||||
|
|
||||||
# Encode key to match network
|
# Encode key to match network
|
||||||
wif_prefix = chainparams[Coins.PART][self.chain]["key_prefix"]
|
wif_prefix = chainparams[Coins.PART][self.chain]["key_prefix"]
|
||||||
@@ -736,24 +750,32 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
|
|||||||
self.delay_event.set()
|
self.delay_event.set()
|
||||||
self.chainstate_delay_event.set()
|
self.chainstate_delay_event.set()
|
||||||
|
|
||||||
if self._network:
|
for coin_type, cc in self.coin_clients.items():
|
||||||
self._network.stopNetwork()
|
interface = cc.get("interface")
|
||||||
self._network = None
|
if (
|
||||||
|
interface
|
||||||
for coin_type, interface in self.coin_interfaces.items():
|
and hasattr(interface, "_backend")
|
||||||
if hasattr(interface, "_backend") and interface._backend is not None:
|
and interface._backend is not None
|
||||||
|
):
|
||||||
try:
|
try:
|
||||||
if hasattr(interface._backend, "_server"):
|
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}")
|
self.log.debug(f"Disconnected electrum backend for {coin_type}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log.debug(f"Error disconnecting electrum backend: {e}")
|
self.log.debug(f"Error disconnecting electrum backend: {e}")
|
||||||
|
|
||||||
|
if self._network:
|
||||||
|
self._network.stopNetwork()
|
||||||
|
self._network = None
|
||||||
|
|
||||||
self.log.info("Stopping threads.")
|
self.log.info("Stopping threads.")
|
||||||
for t in self.threads:
|
for t in self.threads:
|
||||||
if hasattr(t, "stop") and callable(t.stop):
|
if hasattr(t, "stop") and callable(t.stop):
|
||||||
t.stop()
|
t.stop()
|
||||||
t.join()
|
t.join(timeout=15)
|
||||||
|
|
||||||
if sys.version_info[1] >= 9:
|
if sys.version_info[1] >= 9:
|
||||||
self.thread_pool.shutdown(cancel_futures=True)
|
self.thread_pool.shutdown(cancel_futures=True)
|
||||||
@@ -1927,15 +1949,29 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
|
|||||||
cc["cached_total_balance"] = current_total
|
cc["cached_total_balance"] = current_total
|
||||||
cc["cached_unconfirmed"] = current_total - current_balance
|
cc["cached_unconfirmed"] = current_total - current_balance
|
||||||
|
|
||||||
balance_event = {
|
suppress = False
|
||||||
"event": "coin_balance_updated",
|
if hasattr(ci, "getBackend") and ci.useBackend():
|
||||||
"coin": ci.ticker(),
|
backend = ci.getBackend()
|
||||||
"height": cc.get("chain_height", 0),
|
if backend and hasattr(backend, "recentlyReconnected"):
|
||||||
"trigger": "electrum_notification",
|
if backend.recentlyReconnected(grace_seconds=30):
|
||||||
"address": address[:20] + "..." if address else None,
|
suppress = True
|
||||||
}
|
|
||||||
self.ws_server.send_message_to_all(json.dumps(balance_event))
|
if suppress:
|
||||||
self.log.debug(f"Electrum notification: {ci.ticker()} balance updated")
|
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:
|
except Exception as e:
|
||||||
self.log.debug(f"Error handling electrum notification: {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"
|
f"will sync keypool and trigger rescan in full node"
|
||||||
)
|
)
|
||||||
return {
|
return {
|
||||||
"empty": True,
|
"empty": False,
|
||||||
|
"reason": "has_balance",
|
||||||
"has_balance": True,
|
"has_balance": True,
|
||||||
"balance_sats": balance_sats,
|
"balance_sats": balance_sats,
|
||||||
"message": (
|
"message": (
|
||||||
@@ -2495,9 +2532,7 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
|
|||||||
else:
|
else:
|
||||||
self.log.warning(f"Sweep skipped for {coin_name}: {reason}")
|
self.log.warning(f"Sweep skipped for {coin_name}: {reason}")
|
||||||
elif result.get("txid"):
|
elif result.get("txid"):
|
||||||
self.log.info(
|
pass
|
||||||
f"Sweep completed: {result.get('amount', 0) / 1e8:.8f} {coin_name} swept to RPC wallet"
|
|
||||||
)
|
|
||||||
elif result.get("error"):
|
elif result.get("error"):
|
||||||
self.log.warning(
|
self.log.warning(
|
||||||
f"Sweep failed for {coin_name}: {result.get('error')}"
|
f"Sweep failed for {coin_name}: {result.get('error')}"
|
||||||
@@ -2745,6 +2780,7 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
|
|||||||
{
|
{
|
||||||
"coin_type": int(coin_type),
|
"coin_type": int(coin_type),
|
||||||
"coin_name": coin_name,
|
"coin_name": coin_name,
|
||||||
|
"ticker": chainparams[coin_type]["ticker"],
|
||||||
"amount": send_amount / 1e8,
|
"amount": send_amount / 1e8,
|
||||||
"fee": fee / 1e8,
|
"fee": fee / 1e8,
|
||||||
"txid": txid_hex,
|
"txid": txid_hex,
|
||||||
@@ -2823,15 +2859,27 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {"error": f"Failed to list UTXOs: {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 = []
|
legacy_utxos = []
|
||||||
total_legacy_sats = 0
|
total_legacy_sats = 0
|
||||||
|
|
||||||
for u in unspent:
|
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
|
continue
|
||||||
addr = u["address"]
|
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)
|
legacy_utxos.append(u)
|
||||||
total_legacy_sats += ci.make_int(u.get("amount", 0))
|
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})",
|
"reason": f"Legacy balance ({total_legacy_sats}) too low for fee ({estimated_fee_sats})",
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
new_address = None
|
||||||
new_address = ci.rpc_wallet("getnewaddress", ["consolidate", "bech32"])
|
if wm:
|
||||||
except Exception as e:
|
try:
|
||||||
return {"error": f"Failed to get new address: {e}"}
|
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_sats = total_legacy_sats - estimated_fee_sats
|
||||||
send_amount_btc = ci.format_amount(send_amount_sats)
|
send_amount_btc = ci.format_amount(send_amount_sats)
|
||||||
@@ -2994,6 +3058,12 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def getElectrumLegacyFundsInfo(self, coin_type: Coins) -> dict:
|
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:
|
try:
|
||||||
cc = self.coin_clients.get(coin_type)
|
cc = self.coin_clients.get(coin_type)
|
||||||
if not cc or cc.get("connection_type") != "electrum":
|
if not cc or cc.get("connection_type") != "electrum":
|
||||||
@@ -3027,7 +3097,7 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
|
|||||||
}
|
}
|
||||||
return {"has_legacy_funds": False}
|
return {"has_legacy_funds": False}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log.debug(f"getElectrumLegacyFundsInfo error: {e}")
|
self.log.debug(f"_computeElectrumLegacyFundsInfo error: {e}")
|
||||||
return {"has_legacy_funds": False}
|
return {"has_legacy_funds": False}
|
||||||
|
|
||||||
def _tryGetFullNodeAddresses(self, coin_type: Coins) -> list:
|
def _tryGetFullNodeAddresses(self, coin_type: Coins) -> list:
|
||||||
@@ -4528,10 +4598,12 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
|
|||||||
self.log.info_s(f"In txn: {txid}")
|
self.log.info_s(f"In txn: {txid}")
|
||||||
return txid
|
return txid
|
||||||
|
|
||||||
def withdrawLTC(self, type_from, value, addr_to, subfee: bool) -> str:
|
def withdrawCoinExtended(
|
||||||
ci = self.ci(Coins.LTC)
|
self, coin_type, type_from, value, addr_to, subfee: bool
|
||||||
|
) -> str:
|
||||||
|
ci = self.ci(coin_type)
|
||||||
self.log.info(
|
self.log.info(
|
||||||
"withdrawLTC{}".format(
|
"withdrawCoinExtended{}".format(
|
||||||
""
|
""
|
||||||
if self.log.safe_logs
|
if self.log.safe_logs
|
||||||
else " {} {} to {} {}".format(
|
else " {} {} to {} {}".format(
|
||||||
@@ -7454,6 +7526,7 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
|
|||||||
dest_address,
|
dest_address,
|
||||||
bid.amount_to,
|
bid.amount_to,
|
||||||
bid.chain_b_height_start,
|
bid.chain_b_height_start,
|
||||||
|
find_index=True,
|
||||||
vout=bid.xmr_b_lock_tx.vout,
|
vout=bid.xmr_b_lock_tx.vout,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@@ -7498,7 +7571,7 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
|
|||||||
bid_id=bid.bid_id,
|
bid_id=bid.bid_id,
|
||||||
tx_type=TxTypes.XMR_SWAP_B_LOCK,
|
tx_type=TxTypes.XMR_SWAP_B_LOCK,
|
||||||
txid=xmr_swap.b_lock_tx_id,
|
txid=xmr_swap.b_lock_tx_id,
|
||||||
vout=0,
|
vout=found_tx.get("index", 0),
|
||||||
)
|
)
|
||||||
if bid.xmr_b_lock_tx.txid != found_txid:
|
if bid.xmr_b_lock_tx.txid != found_txid:
|
||||||
self.log.debug(
|
self.log.debug(
|
||||||
@@ -9328,6 +9401,95 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
|
|||||||
|
|
||||||
c["last_height_checked"] = chain_blocks
|
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:
|
def expireMessageRoutes(self) -> None:
|
||||||
if self._is_locked is True:
|
if self._is_locked is True:
|
||||||
self.log.debug("Not expiring message routes while system is locked")
|
self.log.debug("Not expiring message routes while system is locked")
|
||||||
@@ -9526,11 +9688,12 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
|
|||||||
try:
|
try:
|
||||||
cursor = self.openDB()
|
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()
|
rows = cursor.execute(query, {"now": now}).fetchall()
|
||||||
|
retry_action_ids = []
|
||||||
|
|
||||||
for row in rows:
|
for row in rows:
|
||||||
action_type, linked_id = row
|
action_id, action_type, linked_id = row
|
||||||
accepting_bid: bool = False
|
accepting_bid: bool = False
|
||||||
try:
|
try:
|
||||||
if action_type == ActionTypes.ACCEPT_BID:
|
if action_type == ActionTypes.ACCEPT_BID:
|
||||||
@@ -9562,6 +9725,11 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
|
|||||||
self.acceptADSReverseBid(linked_id, cursor)
|
self.acceptADSReverseBid(linked_id, cursor)
|
||||||
else:
|
else:
|
||||||
self.log.warning(f"Unknown event type: {action_type}")
|
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:
|
except Exception as ex:
|
||||||
err_msg = f"checkQueuedActions failed: {ex}"
|
err_msg = f"checkQueuedActions failed: {ex}"
|
||||||
self.logException(err_msg)
|
self.logException(err_msg)
|
||||||
@@ -9594,10 +9762,23 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
|
|||||||
bid.setState(BidStates.BID_ERROR)
|
bid.setState(BidStates.BID_ERROR)
|
||||||
self.saveBidInSession(bid_id, bid, cursor)
|
self.saveBidInSession(bid_id, bid, cursor)
|
||||||
|
|
||||||
query: str = "DELETE FROM actions WHERE trigger_at <= :now"
|
if retry_action_ids:
|
||||||
if self.debug:
|
placeholders = ",".join(
|
||||||
query = "UPDATE actions SET active_ind = 2 WHERE trigger_at <= :now"
|
f":retry_{i}" for i in range(len(retry_action_ids))
|
||||||
cursor.execute(query, {"now": now})
|
)
|
||||||
|
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:
|
except Exception as ex:
|
||||||
self.handleSessionErrors(ex, cursor, "checkQueuedActions")
|
self.handleSessionErrors(ex, cursor, "checkQueuedActions")
|
||||||
@@ -11349,13 +11530,18 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
|
|||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
b_lock_tx_id = ci_to.publishBLockTx(
|
b_lock_vout = 0
|
||||||
|
result = ci_to.publishBLockTx(
|
||||||
xmr_swap.vkbv,
|
xmr_swap.vkbv,
|
||||||
xmr_swap.pkbs,
|
xmr_swap.pkbs,
|
||||||
bid.amount_to,
|
bid.amount_to,
|
||||||
b_fee_rate,
|
b_fee_rate,
|
||||||
unlock_time=unlock_time,
|
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:
|
if bid.debug_ind == DebugTypes.B_LOCK_TX_MISSED_SEND:
|
||||||
self.log.debug(
|
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)}."
|
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,
|
bid_id=bid_id,
|
||||||
tx_type=TxTypes.XMR_SWAP_B_LOCK,
|
tx_type=TxTypes.XMR_SWAP_B_LOCK,
|
||||||
txid=b_lock_tx_id,
|
txid=b_lock_tx_id,
|
||||||
vout=0,
|
vout=b_lock_vout,
|
||||||
)
|
)
|
||||||
xmr_swap.b_lock_tx_id = b_lock_tx_id
|
xmr_swap.b_lock_tx_id = b_lock_tx_id
|
||||||
bid.xmr_b_lock_tx.setState(TxStates.TX_SENT)
|
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:
|
if self._zmq_queue_enabled and self.zmqSubscriber:
|
||||||
try:
|
try:
|
||||||
if self._read_zmq_queue:
|
if self._read_zmq_queue:
|
||||||
topic, message, seq = self.zmqSubscriber.recv_multipart(
|
for _i in range(100):
|
||||||
flags=zmq.NOBLOCK
|
topic, message, seq = self.zmqSubscriber.recv_multipart(
|
||||||
)
|
flags=zmq.NOBLOCK
|
||||||
if topic == b"smsg":
|
)
|
||||||
self.processZmqSmsg(message)
|
if topic == b"smsg":
|
||||||
elif topic == b"hashwtx":
|
self.processZmqSmsg(message)
|
||||||
self.processZmqHashwtx(message)
|
elif topic == b"hashwtx":
|
||||||
|
self.processZmqHashwtx(message)
|
||||||
except zmq.Again as e: # noqa: F841
|
except zmq.Again as e: # noqa: F841
|
||||||
pass
|
pass
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logException(f"smsg zmq {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()
|
self.updateNetwork()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -12891,7 +13089,21 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
|
|||||||
):
|
):
|
||||||
continue
|
continue
|
||||||
if len(c["watched_outputs"]) > 0 or len(c["watched_scripts"]):
|
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
|
self._last_checked_watched = now
|
||||||
|
|
||||||
if now - self._last_checked_expired >= self.check_expired_seconds:
|
if now - self._last_checked_expired >= self.check_expired_seconds:
|
||||||
@@ -13250,39 +13462,35 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
|
|||||||
display_name = getCoinName(coin_id)
|
display_name = getCoinName(coin_id)
|
||||||
|
|
||||||
if old_connection_type == "rpc" and new_connection_type == "electrum":
|
if old_connection_type == "rpc" and new_connection_type == "electrum":
|
||||||
auto_transfer_now = data.get("auto_transfer_now", False)
|
|
||||||
if auto_transfer_now:
|
|
||||||
transfer_result = self._consolidateLegacyFundsToSegwit(coin_id)
|
|
||||||
if transfer_result.get("success"):
|
|
||||||
self.log.info(
|
|
||||||
f"Consolidated {transfer_result.get('amount', 0):.8f} {display_name} "
|
|
||||||
f"from legacy addresses. TXID: {transfer_result.get('txid')}"
|
|
||||||
)
|
|
||||||
if migration_message:
|
|
||||||
migration_message += f" Transferred {transfer_result.get('amount', 0):.8f} {display_name} to native segwit."
|
|
||||||
else:
|
|
||||||
migration_message = f"Transferred {transfer_result.get('amount', 0):.8f} {display_name} to native segwit."
|
|
||||||
elif transfer_result.get("skipped"):
|
|
||||||
self.log.info(
|
|
||||||
f"Legacy fund transfer skipped for {coin_name}: {transfer_result.get('reason')}"
|
|
||||||
)
|
|
||||||
elif transfer_result.get("error"):
|
|
||||||
self.log.warning(
|
|
||||||
f"Legacy fund transfer warning for {coin_name}: {transfer_result.get('error')}"
|
|
||||||
)
|
|
||||||
|
|
||||||
migration_result = self._migrateWalletToLiteMode(coin_id)
|
migration_result = self._migrateWalletToLiteMode(coin_id)
|
||||||
if migration_result.get("success"):
|
if migration_result.get("success"):
|
||||||
count = migration_result.get("count", 0)
|
count = migration_result.get("count", 0)
|
||||||
self.log.info(
|
self.log.info(
|
||||||
f"Lite wallet ready for {coin_name} with {count} addresses"
|
f"Lite wallet ready for {coin_name} with {count} addresses"
|
||||||
)
|
)
|
||||||
if migration_message:
|
migration_message = (
|
||||||
migration_message += (
|
f"Lite wallet ready for {display_name} ({count} addresses)."
|
||||||
f" Lite wallet ready ({count} addresses)."
|
)
|
||||||
|
|
||||||
|
auto_transfer_now = data.get("auto_transfer_now", False)
|
||||||
|
if auto_transfer_now:
|
||||||
|
transfer_result = self._consolidateLegacyFundsToSegwit(
|
||||||
|
coin_id
|
||||||
)
|
)
|
||||||
else:
|
if transfer_result.get("success"):
|
||||||
migration_message = f"Lite wallet ready for {display_name} ({count} addresses)."
|
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:
|
else:
|
||||||
error = migration_result.get("error", "unknown")
|
error = migration_result.get("error", "unknown")
|
||||||
reason = migration_result.get("reason", "")
|
reason = migration_result.get("reason", "")
|
||||||
@@ -13333,14 +13541,23 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
|
|||||||
f"Transfer failed for {coin_name}: {error}"
|
f"Transfer failed for {coin_name}: {error}"
|
||||||
)
|
)
|
||||||
raise ValueError(f"Transfer failed: {error}")
|
raise ValueError(f"Transfer failed: {error}")
|
||||||
elif reason in ("has_balance", "active_swap"):
|
elif reason == "active_swap":
|
||||||
error = empty_check.get(
|
error = empty_check.get(
|
||||||
"message", "Wallet must be empty before switching modes"
|
"message", "Cannot switch: active swap in progress"
|
||||||
)
|
)
|
||||||
self.log.error(
|
self.log.error(
|
||||||
f"Migration blocked for {coin_name}: {error}"
|
f"Migration blocked for {coin_name}: {error}"
|
||||||
)
|
)
|
||||||
raise ValueError(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)
|
sync_result = self._syncWalletIndicesToRPC(coin_id)
|
||||||
if sync_result.get("success"):
|
if sync_result.get("success"):
|
||||||
@@ -13712,6 +13929,26 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
|
|||||||
rv["mweb_pending"] = walletinfo.get(
|
rv["mweb_pending"] = walletinfo.get(
|
||||||
"mweb_unconfirmed", 0
|
"mweb_unconfirmed", 0
|
||||||
) + walletinfo.get("mweb_immature", 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
|
return rv
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -13719,9 +13956,9 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
|
|||||||
|
|
||||||
def addWalletInfoRecord(self, coin, info_type, wi) -> None:
|
def addWalletInfoRecord(self, coin, info_type, wi) -> None:
|
||||||
coin_id = int(coin)
|
coin_id = int(coin)
|
||||||
|
now: int = self.getTime()
|
||||||
cursor = self.openDB()
|
cursor = self.openDB()
|
||||||
try:
|
try:
|
||||||
now: int = self.getTime()
|
|
||||||
self.add(
|
self.add(
|
||||||
Wallets(
|
Wallets(
|
||||||
coin_id=coin,
|
coin_id=coin,
|
||||||
@@ -13975,6 +14212,8 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
|
|||||||
if row2[0].startswith("stealth"):
|
if row2[0].startswith("stealth"):
|
||||||
if coin_id == Coins.LTC:
|
if coin_id == Coins.LTC:
|
||||||
wallet_data["mweb_address"] = row2[1]
|
wallet_data["mweb_address"] = row2[1]
|
||||||
|
elif coin_id == Coins.FIRO:
|
||||||
|
wallet_data["spark_address"] = row2[1]
|
||||||
else:
|
else:
|
||||||
wallet_data["stealth_address"] = row2[1]
|
wallet_data["stealth_address"] = row2[1]
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -36,22 +36,25 @@ def signal_handler(sig, frame):
|
|||||||
os.write(
|
os.write(
|
||||||
sys.stdout.fileno(), f"Signal {sig} detected, ending program.\n".encode("utf-8")
|
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:
|
||||||
try:
|
if swap_client is not None and not swap_client.chainstate_delay_event.is_set():
|
||||||
from basicswap.ui.page_amm import stop_amm_process, get_amm_status
|
try:
|
||||||
|
from basicswap.ui.page_amm import stop_amm_process, get_amm_status
|
||||||
|
|
||||||
amm_status = get_amm_status()
|
amm_status = get_amm_status()
|
||||||
if amm_status == "running":
|
if amm_status == "running":
|
||||||
logger.info("Signal handler stopping AMM process...")
|
logger.info("Signal handler stopping AMM process...")
|
||||||
success, msg = stop_amm_process(swap_client)
|
success, msg = stop_amm_process(swap_client)
|
||||||
if success:
|
if success:
|
||||||
logger.info(f"AMM signal shutdown: {msg}")
|
logger.info(f"AMM signal shutdown: {msg}")
|
||||||
else:
|
else:
|
||||||
logger.warning(f"AMM signal shutdown warning: {msg}")
|
logger.warning(f"AMM signal shutdown warning: {msg}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error stopping AMM in signal handler: {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):
|
def checkPARTZmqConfigBeforeStart(part_settings, swap_settings):
|
||||||
@@ -618,7 +621,7 @@ def runClient(
|
|||||||
signal.CTRL_C_EVENT if os.name == "nt" else signal.SIGINT
|
signal.CTRL_C_EVENT if os.name == "nt" else signal.SIGINT
|
||||||
)
|
)
|
||||||
except Exception as e:
|
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:
|
for d in daemons:
|
||||||
try:
|
try:
|
||||||
d.handle.wait(timeout=120)
|
d.handle.wait(timeout=120)
|
||||||
@@ -627,10 +630,12 @@ def runClient(
|
|||||||
fp.close()
|
fp.close()
|
||||||
closed_pids.append(d.handle.pid)
|
closed_pids.append(d.handle.pid)
|
||||||
except Exception as e:
|
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
|
fail_code: int = swap_client.fail_code
|
||||||
del swap_client
|
swap_client = None
|
||||||
|
|
||||||
if os.path.exists(pids_path):
|
if os.path.exists(pids_path):
|
||||||
with open(pids_path) as fd:
|
with open(pids_path) as fd:
|
||||||
|
|||||||
@@ -6,8 +6,10 @@
|
|||||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import gzip
|
||||||
import json
|
import json
|
||||||
import shlex
|
import shlex
|
||||||
|
import hashlib
|
||||||
import secrets
|
import secrets
|
||||||
import traceback
|
import traceback
|
||||||
import threading
|
import threading
|
||||||
@@ -19,6 +21,7 @@ from jinja2 import Environment, PackageLoader
|
|||||||
from socket import error as SocketError
|
from socket import error as SocketError
|
||||||
from urllib import parse
|
from urllib import parse
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
|
from email.utils import formatdate, parsedate_to_datetime
|
||||||
from http.cookies import SimpleCookie
|
from http.cookies import SimpleCookie
|
||||||
|
|
||||||
from . import __version__
|
from . import __version__
|
||||||
@@ -802,7 +805,6 @@ class HttpHandler(BaseHTTPRequestHandler):
|
|||||||
if page == "static":
|
if page == "static":
|
||||||
try:
|
try:
|
||||||
static_path = os.path.join(os.path.dirname(__file__), "static")
|
static_path = os.path.join(os.path.dirname(__file__), "static")
|
||||||
content = None
|
|
||||||
mime_type = ""
|
mime_type = ""
|
||||||
filepath = ""
|
filepath = ""
|
||||||
if len(url_split) > 3 and url_split[2] == "sequence_diagrams":
|
if len(url_split) > 3 and url_split[2] == "sequence_diagrams":
|
||||||
@@ -835,9 +837,73 @@ class HttpHandler(BaseHTTPRequestHandler):
|
|||||||
if mime_type == "" or not filepath:
|
if mime_type == "" or not filepath:
|
||||||
raise ValueError("Unknown file type or path")
|
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:
|
with open(filepath, "rb") as fp:
|
||||||
content = fp.read()
|
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
|
return content
|
||||||
|
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import os
|
|||||||
import shutil
|
import shutil
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import threading
|
import threading
|
||||||
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
@@ -185,6 +186,7 @@ def extractScriptLockRefundScriptValues(script_bytes: bytes):
|
|||||||
|
|
||||||
class BTCInterface(Secp256k1Interface):
|
class BTCInterface(Secp256k1Interface):
|
||||||
_scantxoutset_lock = threading.Lock()
|
_scantxoutset_lock = threading.Lock()
|
||||||
|
_MAX_SCANTXOUTSET_RETRIES = 3
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def coin_type():
|
def coin_type():
|
||||||
@@ -233,6 +235,10 @@ class BTCInterface(Secp256k1Interface):
|
|||||||
def xmr_swap_b_lock_spend_tx_vsize() -> int:
|
def xmr_swap_b_lock_spend_tx_vsize() -> int:
|
||||||
return 110
|
return 110
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def getdustlimit() -> int:
|
||||||
|
return 5460
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def txoType():
|
def txoType():
|
||||||
return CTxOut
|
return CTxOut
|
||||||
@@ -759,6 +765,19 @@ class BTCInterface(Secp256k1Interface):
|
|||||||
wm.syncBalances(
|
wm.syncBalances(
|
||||||
self.coin_type(), self._backend, funded_only=not do_full_scan
|
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:
|
finally:
|
||||||
if hasattr(self._backend, "setBackgroundMode"):
|
if hasattr(self._backend, "setBackgroundMode"):
|
||||||
self._backend.setBackgroundMode(False)
|
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_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_fee = max(round(feerate_satkb * rough_vsize / 1000), min_relay_fee)
|
||||||
rough_change = total_input - total_output - rough_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())
|
change_addr = wm.getNewInternalAddress(self.coin_type())
|
||||||
if not change_addr:
|
if not change_addr:
|
||||||
change_addr = wm.getExistingInternalAddress(self.coin_type())
|
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)
|
final_fee = max(round(feerate_satkb * final_vsize / 1000), min_relay_fee)
|
||||||
change = total_input - total_output - final_fee
|
change = total_input - total_output - final_fee
|
||||||
|
|
||||||
if change > 1000:
|
if change > dust_limit:
|
||||||
funded_tx.vout[-1].nValue = change
|
funded_tx.vout[-1].nValue = change
|
||||||
else:
|
else:
|
||||||
funded_tx.vout.pop()
|
funded_tx.vout.pop()
|
||||||
@@ -2418,10 +2438,37 @@ class BTCInterface(Secp256k1Interface):
|
|||||||
def getPkDest(self, K: bytes) -> bytearray:
|
def getPkDest(self, K: bytes) -> bytearray:
|
||||||
return self.getScriptForPubkeyHash(self.getPubkeyHash(K))
|
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):
|
def scanTxOutset(self, dest):
|
||||||
if self._connection_type == "electrum":
|
if self._connection_type == "electrum":
|
||||||
return self._scanTxOutsetElectrum(dest)
|
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):
|
def _scanTxOutsetElectrum(self, dest):
|
||||||
backend = self.getBackend()
|
backend = self.getBackend()
|
||||||
@@ -2572,15 +2619,19 @@ class BTCInterface(Secp256k1Interface):
|
|||||||
def encodeSharedAddress(self, Kbv, Kbs):
|
def encodeSharedAddress(self, Kbv, Kbs):
|
||||||
return self.pubkey_to_segwit_address(Kbs)
|
return self.pubkey_to_segwit_address(Kbs)
|
||||||
|
|
||||||
def publishBLockTx(
|
def publishBLockTx(self, kbv, Kbs, output_amount, feerate, unlock_time: int = 0):
|
||||||
self, kbv, Kbs, output_amount, feerate, unlock_time: int = 0
|
|
||||||
) -> bytes:
|
|
||||||
b_lock_tx = self.createBLockTx(Kbs, output_amount)
|
b_lock_tx = self.createBLockTx(Kbs, output_amount)
|
||||||
|
|
||||||
b_lock_tx = self.fundTx(b_lock_tx, feerate)
|
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)
|
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:
|
def getTxVSize(self, tx, add_bytes: int = 0, add_witness_bytes: int = 0) -> int:
|
||||||
wsf = self.witnessScaleFactor()
|
wsf = self.witnessScaleFactor()
|
||||||
@@ -2604,7 +2655,9 @@ class BTCInterface(Secp256k1Interface):
|
|||||||
if self.using_segwit()
|
if self.using_segwit()
|
||||||
else self.pubkey_to_address(Kbs)
|
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)
|
raw_dest = self.getPkDest(Kbs)
|
||||||
@@ -2646,44 +2699,66 @@ class BTCInterface(Secp256k1Interface):
|
|||||||
self._log.id(chain_b_lock_txid), lock_tx_vout
|
self._log.id(chain_b_lock_txid), lock_tx_vout
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
locked_n = lock_tx_vout
|
|
||||||
|
|
||||||
Kbs = self.getPubkey(kbs)
|
Kbs = self.getPubkey(kbs)
|
||||||
script_pk = self.getPkDest(Kbs)
|
script_pk = self.getPkDest(Kbs)
|
||||||
|
|
||||||
if locked_n is None:
|
locked_n = None
|
||||||
if self.useBackend():
|
actual_value = None
|
||||||
backend = self.getBackend()
|
if self.useBackend():
|
||||||
tx_hex = backend.getTransactionRaw(chain_b_lock_txid.hex())
|
backend = self.getBackend()
|
||||||
if tx_hex:
|
tx_hex = backend.getTransactionRaw(chain_b_lock_txid.hex())
|
||||||
lock_tx = self.loadTx(bytes.fromhex(tx_hex))
|
if tx_hex:
|
||||||
locked_n = findOutput(lock_tx, script_pk)
|
lock_tx = self.loadTx(bytes.fromhex(tx_hex))
|
||||||
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 = findOutput(lock_tx, script_pk)
|
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")
|
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)
|
pkh_to = self.decodeAddress(address_to)
|
||||||
|
|
||||||
tx = CTransaction()
|
tx = CTransaction()
|
||||||
@@ -2699,16 +2774,14 @@ class BTCInterface(Secp256k1Interface):
|
|||||||
scriptSig=self.getScriptScriptSig(script_lock),
|
scriptSig=self.getScriptScriptSig(script_lock),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
tx.vout.append(
|
tx.vout.append(self.txoType()(spend_value, self.getScriptForPubkeyHash(pkh_to)))
|
||||||
self.txoType()(cb_swap_value, self.getScriptForPubkeyHash(pkh_to))
|
|
||||||
)
|
|
||||||
|
|
||||||
pay_fee = self.getBLockSpendTxFee(tx, b_fee)
|
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 = tx.serialize()
|
||||||
b_lock_spend_tx = self.signTxWithKey(
|
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))
|
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)
|
return self._getOutputElectrum(txid, dest_script, expect_value, xmr_swap)
|
||||||
|
|
||||||
# TODO: Use getrawtransaction if txindex is active
|
# TODO: Use getrawtransaction if txindex is active
|
||||||
utxos = self.rpc(
|
utxos = self._rpc_scantxoutset(["raw({})".format(dest_script.hex())])
|
||||||
"scantxoutset", ["start", ["raw({})".format(dest_script.hex())]]
|
|
||||||
)
|
|
||||||
if "height" in utxos: # chain_height not returned by v18 codebase
|
if "height" in utxos: # chain_height not returned by v18 codebase
|
||||||
chain_height = utxos["height"]
|
chain_height = utxos["height"]
|
||||||
else:
|
else:
|
||||||
@@ -3492,13 +3563,12 @@ class BTCInterface(Secp256k1Interface):
|
|||||||
|
|
||||||
sum_unspent = 0
|
sum_unspent = 0
|
||||||
|
|
||||||
with BTCInterface._scantxoutset_lock:
|
self._log.debug("scantxoutset start")
|
||||||
self._log.debug("scantxoutset start")
|
ro = self._rpc_scantxoutset(["addr({})".format(address)])
|
||||||
ro = self.rpc("scantxoutset", ["start", ["addr({})".format(address)]])
|
self._log.debug("scantxoutset end")
|
||||||
self._log.debug("scantxoutset end")
|
|
||||||
|
|
||||||
for o in ro["unspents"]:
|
for o in ro["unspents"]:
|
||||||
sum_unspent += self.make_int(o["amount"])
|
sum_unspent += self.make_int(o["amount"])
|
||||||
return sum_unspent
|
return sum_unspent
|
||||||
|
|
||||||
def _getUTXOBalanceElectrum(self, address: str):
|
def _getUTXOBalanceElectrum(self, address: str):
|
||||||
@@ -3881,7 +3951,7 @@ class BTCInterface(Secp256k1Interface):
|
|||||||
|
|
||||||
self.rpc("loadwallet", [self._rpc_wallet])
|
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:
|
if check_seed is False or seed_id_before == "Not found" or walletpath is None:
|
||||||
return
|
return
|
||||||
@@ -4005,7 +4075,9 @@ class BTCInterface(Secp256k1Interface):
|
|||||||
if self.isWalletEncrypted():
|
if self.isWalletEncrypted():
|
||||||
raise ValueError("Old password must be set")
|
raise ValueError("Old password must be set")
|
||||||
return self.encryptWallet(new_password, check_seed=check_seed_if_encrypt)
|
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:
|
def unlockWallet(self, password: str, check_seed: bool = True) -> None:
|
||||||
if password == "":
|
if password == "":
|
||||||
@@ -4038,12 +4110,9 @@ class BTCInterface(Secp256k1Interface):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
seed_id = self.getWalletSeedID()
|
seed_id = self.getWalletSeedID()
|
||||||
self._log.debug(
|
|
||||||
f"{self.ticker()} unlockWallet getWalletSeedID returned: {seed_id}"
|
|
||||||
)
|
|
||||||
needs_seed_init = seed_id == "Not found"
|
needs_seed_init = seed_id == "Not found"
|
||||||
except Exception as e:
|
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
|
needs_seed_init = True
|
||||||
if needs_seed_init:
|
if needs_seed_init:
|
||||||
self._log.info(f"Initializing HD seed for {self.coin_name()}.")
|
self._log.info(f"Initializing HD seed for {self.coin_name()}.")
|
||||||
@@ -4051,11 +4120,9 @@ class BTCInterface(Secp256k1Interface):
|
|||||||
if password:
|
if password:
|
||||||
self._log.info(f"Encrypting {self.coin_name()} wallet.")
|
self._log.info(f"Encrypting {self.coin_name()} wallet.")
|
||||||
try:
|
try:
|
||||||
self.rpc_wallet("encryptwallet", [password])
|
self.rpc_wallet("encryptwallet", [password], timeout=120)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self._log.debug(f"encryptwallet returned: {e}")
|
self._log.debug(f"encryptwallet returned: {e}")
|
||||||
import time
|
|
||||||
|
|
||||||
for i in range(10):
|
for i in range(10):
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
try:
|
try:
|
||||||
@@ -4072,7 +4139,7 @@ class BTCInterface(Secp256k1Interface):
|
|||||||
check_seed = False
|
check_seed = False
|
||||||
|
|
||||||
if self.isWalletEncrypted():
|
if self.isWalletEncrypted():
|
||||||
self.rpc_wallet("walletpassphrase", [password, 100000000])
|
self.rpc_wallet("walletpassphrase", [password, 100000000], timeout=120)
|
||||||
if check_seed:
|
if check_seed:
|
||||||
self._sc.checkWalletSeed(self.coin_type())
|
self._sc.checkWalletSeed(self.coin_type())
|
||||||
|
|
||||||
@@ -4080,7 +4147,15 @@ class BTCInterface(Secp256k1Interface):
|
|||||||
self._log.info(f"lockWallet - {self.ticker()}")
|
self._log.info(f"lockWallet - {self.ticker()}")
|
||||||
if self.useBackend():
|
if self.useBackend():
|
||||||
return
|
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:
|
def get_p2sh_script_pubkey(self, script: bytearray) -> bytearray:
|
||||||
script_hash = hash160(script)
|
script_hash = hash160(script)
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ class DASHInterface(BTCInterface):
|
|||||||
self.unlockWallet(old_password, check_seed=False)
|
self.unlockWallet(old_password, check_seed=False)
|
||||||
seed_id_before: str = self.getWalletSeedID()
|
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":
|
if check_seed is False or seed_id_before == "Not found":
|
||||||
return
|
return
|
||||||
@@ -156,4 +156,6 @@ class DASHInterface(BTCInterface):
|
|||||||
if self.isWalletEncrypted():
|
if self.isWalletEncrypted():
|
||||||
raise ValueError("Old password must be set")
|
raise ValueError("Old password must be set")
|
||||||
return self.encryptWallet(old_password, new_password, check_seed_if_encrypt)
|
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():
|
def coin_type():
|
||||||
return Coins.DCR
|
return Coins.DCR
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def useBackend() -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def exp() -> int:
|
def exp() -> int:
|
||||||
return 8
|
return 8
|
||||||
@@ -364,7 +368,9 @@ class DCRInterface(Secp256k1Interface):
|
|||||||
# Read initial pwd from settings
|
# Read initial pwd from settings
|
||||||
settings = self._sc.getChainClientSettings(self.coin_type())
|
settings = self._sc.getChainClientSettings(self.coin_type())
|
||||||
old_password = settings["wallet_pwd"]
|
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
|
# Lock wallet to match other coins
|
||||||
self.rpc_wallet("walletlock")
|
self.rpc_wallet("walletlock")
|
||||||
@@ -378,7 +384,7 @@ class DCRInterface(Secp256k1Interface):
|
|||||||
self._log.info("unlockWallet - {}".format(self.ticker()))
|
self._log.info("unlockWallet - {}".format(self.ticker()))
|
||||||
|
|
||||||
# Max timeout value, ~3 years
|
# Max timeout value, ~3 years
|
||||||
self.rpc_wallet("walletpassphrase", [password, 100000000])
|
self.rpc_wallet("walletpassphrase", [password, 100000000], timeout=120)
|
||||||
if check_seed:
|
if check_seed:
|
||||||
self._sc.checkWalletSeed(self.coin_type())
|
self._sc.checkWalletSeed(self.coin_type())
|
||||||
|
|
||||||
@@ -1064,6 +1070,9 @@ class DCRInterface(Secp256k1Interface):
|
|||||||
def describeTx(self, tx_hex: str):
|
def describeTx(self, tx_hex: str):
|
||||||
return self.rpc("decoderawtransaction", [tx_hex])
|
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:
|
def fundTx(self, tx: bytes, feerate) -> bytes:
|
||||||
feerate_str = float(self.format_amount(feerate))
|
feerate_str = float(self.format_amount(feerate))
|
||||||
# TODO: unlock unspents if bid cancelled
|
# TODO: unlock unspents if bid cancelled
|
||||||
@@ -1732,15 +1741,19 @@ class DCRInterface(Secp256k1Interface):
|
|||||||
tx.vout.append(self.txoType()(output_amount, script_pk))
|
tx.vout.append(self.txoType()(output_amount, script_pk))
|
||||||
return tx.serialize()
|
return tx.serialize()
|
||||||
|
|
||||||
def publishBLockTx(
|
def publishBLockTx(self, kbv, Kbs, output_amount, feerate, unlock_time: int = 0):
|
||||||
self, kbv, Kbs, output_amount, feerate, unlock_time: int = 0
|
|
||||||
) -> bytes:
|
|
||||||
b_lock_tx = self.createBLockTx(Kbs, output_amount)
|
b_lock_tx = self.createBLockTx(Kbs, output_amount)
|
||||||
|
|
||||||
b_lock_tx = self.fundTx(b_lock_tx, feerate)
|
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)
|
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:
|
def getBLockSpendTxFee(self, tx, fee_rate: int) -> int:
|
||||||
witness_bytes = 115
|
witness_bytes = 115
|
||||||
@@ -1764,26 +1777,53 @@ class DCRInterface(Secp256k1Interface):
|
|||||||
lock_tx_vout=None,
|
lock_tx_vout=None,
|
||||||
) -> bytes:
|
) -> bytes:
|
||||||
self._log.info("spendBLockTx %s:\n", chain_b_lock_txid.hex())
|
self._log.info("spendBLockTx %s:\n", chain_b_lock_txid.hex())
|
||||||
locked_n = lock_tx_vout
|
|
||||||
|
|
||||||
Kbs = self.getPubkey(kbs)
|
Kbs = self.getPubkey(kbs)
|
||||||
script_pk = self.getPkDest(Kbs)
|
script_pk = self.getPkDest(Kbs)
|
||||||
|
|
||||||
if locked_n is None:
|
locked_n = None
|
||||||
self._log.debug(
|
actual_value = None
|
||||||
f"Unknown lock vout, searching tx: {chain_b_lock_txid.hex()}"
|
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
|
for i, out in enumerate(lock_tx.vout):
|
||||||
wtx = self.rpc_wallet(
|
self._log.debug(
|
||||||
"gettransaction",
|
f" vout[{i}]: value={out.value}, scriptPubKey={out.scriptPubKey.hex()}"
|
||||||
[
|
)
|
||||||
chain_b_lock_txid.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")
|
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)
|
pkh_to = self.decodeAddress(address_to)
|
||||||
|
|
||||||
tx = CTransaction()
|
tx = CTransaction()
|
||||||
@@ -1792,10 +1832,10 @@ class DCRInterface(Secp256k1Interface):
|
|||||||
chain_b_lock_txid_int = b2i(chain_b_lock_txid)
|
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.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)
|
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 = tx.serialize()
|
||||||
b_lock_spend_tx = self.signTxWithKey(b_lock_spend_tx, kbs)
|
b_lock_spend_tx = self.signTxWithKey(b_lock_spend_tx, kbs)
|
||||||
|
|||||||
@@ -119,7 +119,8 @@ class ElectrumConnection:
|
|||||||
self._socket = None
|
self._socket = None
|
||||||
self._connected = False
|
self._connected = False
|
||||||
_close_socket_safe(sock)
|
_close_socket_safe(sock)
|
||||||
for q in self._response_queues.values():
|
queues = list(self._response_queues.values())
|
||||||
|
for q in queues:
|
||||||
try:
|
try:
|
||||||
q.put({"error": "Connection closed"})
|
q.put({"error": "Connection closed"})
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -305,17 +306,26 @@ class ElectrumConnection:
|
|||||||
results = {}
|
results = {}
|
||||||
deadline = time.time() + timeout
|
deadline = time.time() + timeout
|
||||||
for req_id in expected_ids:
|
for req_id in expected_ids:
|
||||||
remaining = deadline - time.time()
|
response = None
|
||||||
if remaining <= 0:
|
while response is None:
|
||||||
raise TemporaryError("Batch request timed out")
|
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:
|
try:
|
||||||
response = self._response_queues[req_id].get(timeout=remaining)
|
|
||||||
if "error" in response and response["error"]:
|
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"]}
|
results[req_id] = {"error": response["error"]}
|
||||||
else:
|
else:
|
||||||
results[req_id] = {"result": response.get("result")}
|
results[req_id] = {"result": response.get("result")}
|
||||||
except queue.Empty:
|
|
||||||
raise TemporaryError("Batch request timed out")
|
|
||||||
finally:
|
finally:
|
||||||
self._response_queues.pop(req_id, None)
|
self._response_queues.pop(req_id, None)
|
||||||
return results
|
return results
|
||||||
@@ -329,13 +339,13 @@ class ElectrumConnection:
|
|||||||
self._request_id += 1
|
self._request_id += 1
|
||||||
request_id = self._request_id
|
request_id = self._request_id
|
||||||
self._response_queues[request_id] = queue.Queue()
|
self._response_queues[request_id] = queue.Queue()
|
||||||
request = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": request_id,
|
"id": request_id,
|
||||||
"method": method,
|
"method": method,
|
||||||
"params": params if params else [],
|
"params": params if params else [],
|
||||||
}
|
}
|
||||||
self._socket.sendall((json.dumps(request) + "\n").encode())
|
self._socket.sendall((json.dumps(request) + "\n").encode())
|
||||||
result = self._receive_response_async(request_id, timeout=timeout)
|
result = self._receive_response_async(request_id, timeout=timeout)
|
||||||
return result
|
return result
|
||||||
else:
|
else:
|
||||||
@@ -470,6 +480,7 @@ class ElectrumServer:
|
|||||||
self._connection = None
|
self._connection = None
|
||||||
self._current_server_idx = 0
|
self._current_server_idx = 0
|
||||||
self._lock = threading.Lock()
|
self._lock = threading.Lock()
|
||||||
|
self._stopping = False
|
||||||
|
|
||||||
self._server_version = None
|
self._server_version = None
|
||||||
self._current_server_host = None
|
self._current_server_host = None
|
||||||
@@ -492,17 +503,24 @@ class ElectrumServer:
|
|||||||
self._server_blacklist = {}
|
self._server_blacklist = {}
|
||||||
self._rate_limit_backoff = 300
|
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_thread = None
|
||||||
self._keepalive_running = False
|
self._keepalive_running = False
|
||||||
self._keepalive_interval = 15
|
self._keepalive_interval = 15
|
||||||
self._last_activity = 0
|
self._last_activity = 0
|
||||||
|
self._last_reconnect_time = 0
|
||||||
|
|
||||||
self._min_request_interval = 0.02
|
self._min_request_interval = 0.02
|
||||||
self._last_request_time = 0
|
self._last_request_time = 0
|
||||||
|
|
||||||
self._bg_connection = None
|
self._user_connection = None
|
||||||
self._bg_lock = threading.Lock()
|
self._user_lock = threading.Lock()
|
||||||
self._bg_last_activity = 0
|
self._user_last_activity = 0
|
||||||
|
self._user_connection_logged = False
|
||||||
|
|
||||||
self._subscribed_height = 0
|
self._subscribed_height = 0
|
||||||
self._subscribed_height_time = 0
|
self._subscribed_height_time = 0
|
||||||
@@ -559,6 +577,8 @@ class ElectrumServer:
|
|||||||
return self._servers[index % len(self._servers)]
|
return self._servers[index % len(self._servers)]
|
||||||
|
|
||||||
def connect(self):
|
def connect(self):
|
||||||
|
if self._stopping:
|
||||||
|
return
|
||||||
sorted_servers = self.get_sorted_servers()
|
sorted_servers = self.get_sorted_servers()
|
||||||
for server in sorted_servers:
|
for server in sorted_servers:
|
||||||
try:
|
try:
|
||||||
@@ -576,6 +596,8 @@ class ElectrumServer:
|
|||||||
version_info = conn.get_server_version()
|
version_info = conn.get_server_version()
|
||||||
if version_info and len(version_info) > 0:
|
if version_info and len(version_info) > 0:
|
||||||
self._server_version = 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_host = server["host"]
|
||||||
self._current_server_port = server["port"]
|
self._current_server_port = server["port"]
|
||||||
self._connection = conn
|
self._connection = conn
|
||||||
@@ -585,6 +607,7 @@ class ElectrumServer:
|
|||||||
self._all_servers_failed = False
|
self._all_servers_failed = False
|
||||||
self._update_server_score(server, success=True, latency_ms=connect_time)
|
self._update_server_score(server, success=True, latency_ms=connect_time)
|
||||||
self._last_activity = time.time()
|
self._last_activity = time.time()
|
||||||
|
self._last_reconnect_time = time.time()
|
||||||
if self._log:
|
if self._log:
|
||||||
if not self._initial_connection_logged:
|
if not self._initial_connection_logged:
|
||||||
self._log.info(
|
self._log.info(
|
||||||
@@ -592,11 +615,15 @@ class ElectrumServer:
|
|||||||
f"({self._server_version}, {connect_time:.0f}ms)"
|
f"({self._server_version}, {connect_time:.0f}ms)"
|
||||||
)
|
)
|
||||||
self._initial_connection_logged = True
|
self._initial_connection_logged = True
|
||||||
else:
|
elif server["host"] != prev_host or server["port"] != prev_port:
|
||||||
self._log.debug(
|
self._log.info(
|
||||||
f"Reconnected to Electrum server: {server['host']}:{server['port']} "
|
f"Switched to Electrum server: {server['host']}:{server['port']} "
|
||||||
f"({connect_time:.0f}ms)"
|
f"({connect_time:.0f}ms)"
|
||||||
)
|
)
|
||||||
|
if self._stopping:
|
||||||
|
conn.disconnect()
|
||||||
|
self._connection = None
|
||||||
|
return
|
||||||
if self._realtime_enabled:
|
if self._realtime_enabled:
|
||||||
self._start_realtime_listener()
|
self._start_realtime_listener()
|
||||||
self._start_keepalive()
|
self._start_keepalive()
|
||||||
@@ -609,8 +636,6 @@ class ElectrumServer:
|
|||||||
self._update_server_score(server, success=False)
|
self._update_server_score(server, success=False)
|
||||||
if self._is_rate_limit_error(str(e)):
|
if self._is_rate_limit_error(str(e)):
|
||||||
self._blacklist_server(server, str(e))
|
self._blacklist_server(server, str(e))
|
||||||
if self._log:
|
|
||||||
self._log.debug(f"Failed to connect to {server['host']}: {e}")
|
|
||||||
continue
|
continue
|
||||||
self._all_servers_failed = True
|
self._all_servers_failed = True
|
||||||
raise TemporaryError(
|
raise TemporaryError(
|
||||||
@@ -673,11 +698,6 @@ class ElectrumServer:
|
|||||||
key = self._get_server_key(s)
|
key = self._get_server_key(s)
|
||||||
if key in self._server_blacklist:
|
if key in self._server_blacklist:
|
||||||
if now < self._server_blacklist[key]:
|
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
|
continue
|
||||||
else:
|
else:
|
||||||
del self._server_blacklist[key]
|
del self._server_blacklist[key]
|
||||||
@@ -728,15 +748,14 @@ class ElectrumServer:
|
|||||||
if self._connection:
|
if self._connection:
|
||||||
self._connection._start_listener()
|
self._connection._start_listener()
|
||||||
result = self._connection.call(
|
result = self._connection.call(
|
||||||
"blockchain.headers.subscribe", [], timeout=10
|
"blockchain.headers.subscribe", [], timeout=20
|
||||||
)
|
)
|
||||||
if result and isinstance(result, dict):
|
if result and isinstance(result, dict):
|
||||||
height = result.get("height", 0)
|
height = result.get("height", 0)
|
||||||
if height > 0:
|
if height > 0:
|
||||||
self._on_header_update(height)
|
self._on_header_update(height)
|
||||||
except Exception as e:
|
except Exception:
|
||||||
if self._log:
|
pass
|
||||||
self._log.debug(f"Failed to subscribe to headers: {e}")
|
|
||||||
|
|
||||||
def register_height_callback(self, callback):
|
def register_height_callback(self, callback):
|
||||||
self._height_callback = callback
|
self._height_callback = callback
|
||||||
@@ -744,6 +763,11 @@ class ElectrumServer:
|
|||||||
def get_subscribed_height(self) -> int:
|
def get_subscribed_height(self) -> int:
|
||||||
return self._subscribed_height
|
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:
|
def get_server_scores(self) -> dict:
|
||||||
return {
|
return {
|
||||||
self._get_server_key(s): {
|
self._get_server_key(s): {
|
||||||
@@ -781,7 +805,8 @@ class ElectrumServer:
|
|||||||
return
|
return
|
||||||
time.sleep(1)
|
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._connection and self._connection.is_connected():
|
||||||
if self._lock.acquire(blocking=False):
|
if self._lock.acquire(blocking=False):
|
||||||
try:
|
try:
|
||||||
@@ -802,6 +827,8 @@ class ElectrumServer:
|
|||||||
self._last_request_time = time.time()
|
self._last_request_time = time.time()
|
||||||
|
|
||||||
def _retry_on_failure(self):
|
def _retry_on_failure(self):
|
||||||
|
if self._stopping:
|
||||||
|
return
|
||||||
self._current_server_idx = (self._current_server_idx + 1) % len(self._servers)
|
self._current_server_idx = (self._current_server_idx + 1) % len(self._servers)
|
||||||
if self._connection:
|
if self._connection:
|
||||||
try:
|
try:
|
||||||
@@ -824,17 +851,27 @@ class ElectrumServer:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def call(self, method, params=None, timeout=10):
|
def call(self, method, params=None, timeout=10):
|
||||||
|
if self._stopping:
|
||||||
|
raise TemporaryError("Electrum server is shutting down")
|
||||||
self._throttle_request()
|
self._throttle_request()
|
||||||
lock_acquired = self._lock.acquire(timeout=timeout + 5)
|
lock_acquired = self._lock.acquire(timeout=timeout + 5)
|
||||||
if not lock_acquired:
|
if not lock_acquired:
|
||||||
raise TemporaryError(f"Electrum call timed out waiting for lock: {method}")
|
raise TemporaryError(f"Electrum call timed out waiting for lock: {method}")
|
||||||
try:
|
try:
|
||||||
for attempt in range(2):
|
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():
|
if self._connection is None or not self._connection.is_connected():
|
||||||
self.connect()
|
self.connect()
|
||||||
|
if self._connection is None:
|
||||||
|
raise TemporaryError("Failed to establish Electrum connection")
|
||||||
elif (time.time() - self._last_activity) > 60:
|
elif (time.time() - self._last_activity) > 60:
|
||||||
if not self._check_connection_health():
|
if not self._check_connection_health():
|
||||||
self._retry_on_failure()
|
self._retry_on_failure()
|
||||||
|
if self._connection is None:
|
||||||
|
raise TemporaryError(
|
||||||
|
"Failed to re-establish Electrum connection"
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
result = self._connection.call(method, params, timeout=timeout)
|
result = self._connection.call(method, params, timeout=timeout)
|
||||||
self._last_activity = time.time()
|
self._last_activity = time.time()
|
||||||
@@ -851,17 +888,27 @@ class ElectrumServer:
|
|||||||
self._lock.release()
|
self._lock.release()
|
||||||
|
|
||||||
def call_batch(self, requests, timeout=15):
|
def call_batch(self, requests, timeout=15):
|
||||||
|
if self._stopping:
|
||||||
|
raise TemporaryError("Electrum server is shutting down")
|
||||||
self._throttle_request()
|
self._throttle_request()
|
||||||
lock_acquired = self._lock.acquire(timeout=timeout + 5)
|
lock_acquired = self._lock.acquire(timeout=timeout + 5)
|
||||||
if not lock_acquired:
|
if not lock_acquired:
|
||||||
raise TemporaryError("Electrum batch call timed out waiting for lock")
|
raise TemporaryError("Electrum batch call timed out waiting for lock")
|
||||||
try:
|
try:
|
||||||
for attempt in range(2):
|
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():
|
if self._connection is None or not self._connection.is_connected():
|
||||||
self.connect()
|
self.connect()
|
||||||
|
if self._connection is None:
|
||||||
|
raise TemporaryError("Failed to establish Electrum connection")
|
||||||
elif (time.time() - self._last_activity) > 60:
|
elif (time.time() - self._last_activity) > 60:
|
||||||
if not self._check_connection_health():
|
if not self._check_connection_health():
|
||||||
self._retry_on_failure()
|
self._retry_on_failure()
|
||||||
|
if self._connection is None:
|
||||||
|
raise TemporaryError(
|
||||||
|
"Failed to re-establish Electrum connection"
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
result = self._connection.call_batch(requests)
|
result = self._connection.call_batch(requests)
|
||||||
self._last_activity = time.time()
|
self._last_activity = time.time()
|
||||||
@@ -877,7 +924,9 @@ class ElectrumServer:
|
|||||||
finally:
|
finally:
|
||||||
self._lock.release()
|
self._lock.release()
|
||||||
|
|
||||||
def _connect_background(self):
|
def _connect_user(self):
|
||||||
|
if self._stopping:
|
||||||
|
return False
|
||||||
sorted_servers = self.get_sorted_servers()
|
sorted_servers = self.get_sorted_servers()
|
||||||
for server in sorted_servers:
|
for server in sorted_servers:
|
||||||
try:
|
try:
|
||||||
@@ -890,105 +939,213 @@ class ElectrumServer:
|
|||||||
proxy_port=self._proxy_port,
|
proxy_port=self._proxy_port,
|
||||||
)
|
)
|
||||||
conn.connect()
|
conn.connect()
|
||||||
self._bg_connection = conn
|
conn.get_server_version()
|
||||||
|
self._user_connection = conn
|
||||||
|
self._user_last_activity = time.time()
|
||||||
if self._log:
|
if self._log:
|
||||||
self._log.debug(
|
if not self._user_connection_logged:
|
||||||
f"Background connection established to {server['host']}"
|
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
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if self._log:
|
if self._log:
|
||||||
self._log.debug(
|
self._log.debug(f"User connection failed to {server['host']}: {e}")
|
||||||
f"Background connection failed to {server['host']}: {e}"
|
|
||||||
)
|
|
||||||
continue
|
continue
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def call_background(self, method, params=None, timeout=10):
|
def _record_timeout(self):
|
||||||
lock_acquired = self._bg_lock.acquire(timeout=1)
|
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:
|
if not lock_acquired:
|
||||||
return self.call(method, params, timeout)
|
raise TemporaryError(f"User connection busy: {method}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if self._bg_connection is None or not self._bg_connection.is_connected():
|
if (
|
||||||
if not self._connect_background():
|
self._user_connection is None
|
||||||
self._bg_lock.release()
|
or not self._user_connection.is_connected()
|
||||||
return self.call(method, params, timeout)
|
):
|
||||||
|
if not self._connect_user():
|
||||||
|
raise TemporaryError("User connection unavailable")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = self._bg_connection.call(method, params, timeout=timeout)
|
result = self._user_connection.call(method, params, timeout=timeout)
|
||||||
self._bg_last_activity = time.time()
|
self._user_last_activity = time.time()
|
||||||
return result
|
return result
|
||||||
except Exception:
|
except Exception as e:
|
||||||
if self._bg_connection:
|
if self._log:
|
||||||
|
self._log.debug(f"User call failed ({method}): {e}")
|
||||||
|
if self._user_connection:
|
||||||
try:
|
try:
|
||||||
self._bg_connection.disconnect()
|
self._user_connection.disconnect()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
self._bg_connection = None
|
self._user_connection = None
|
||||||
|
|
||||||
if self._connect_background():
|
if self._connect_user():
|
||||||
try:
|
try:
|
||||||
result = self._bg_connection.call(
|
result = self._user_connection.call(
|
||||||
method, params, timeout=timeout
|
method, params, timeout=timeout
|
||||||
)
|
)
|
||||||
self._bg_last_activity = time.time()
|
self._user_last_activity = time.time()
|
||||||
return result
|
return result
|
||||||
except Exception:
|
except Exception as e2:
|
||||||
pass
|
raise TemporaryError(f"User call failed: {e2}")
|
||||||
|
|
||||||
return self.call(method, params, timeout)
|
raise TemporaryError(f"User call failed: {e}")
|
||||||
finally:
|
finally:
|
||||||
self._bg_lock.release()
|
self._user_lock.release()
|
||||||
|
|
||||||
def call_batch_background(self, requests, timeout=15):
|
def call_batch_user(self, requests, timeout=15):
|
||||||
lock_acquired = self._bg_lock.acquire(timeout=1)
|
if self._stopping:
|
||||||
|
raise TemporaryError("Electrum server is shutting down")
|
||||||
|
lock_acquired = self._user_lock.acquire(timeout=timeout + 2)
|
||||||
if not lock_acquired:
|
if not lock_acquired:
|
||||||
return self.call_batch(requests, timeout)
|
raise TemporaryError("User connection busy")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if self._bg_connection is None or not self._bg_connection.is_connected():
|
if (
|
||||||
if not self._connect_background():
|
self._user_connection is None
|
||||||
self._bg_lock.release()
|
or not self._user_connection.is_connected()
|
||||||
return self.call_batch(requests, timeout)
|
):
|
||||||
|
if not self._connect_user():
|
||||||
|
raise TemporaryError("User connection unavailable")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = self._bg_connection.call_batch(requests)
|
result = self._user_connection.call_batch(requests)
|
||||||
self._bg_last_activity = time.time()
|
self._user_last_activity = time.time()
|
||||||
return result
|
return result
|
||||||
except Exception:
|
except Exception as e:
|
||||||
if self._bg_connection:
|
if self._log:
|
||||||
|
self._log.debug(f"User batch call failed: {e}")
|
||||||
|
if self._user_connection:
|
||||||
try:
|
try:
|
||||||
self._bg_connection.disconnect()
|
self._user_connection.disconnect()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
self._bg_connection = None
|
self._user_connection = None
|
||||||
|
|
||||||
if self._connect_background():
|
if self._connect_user():
|
||||||
try:
|
try:
|
||||||
result = self._bg_connection.call_batch(requests)
|
result = self._user_connection.call_batch(requests)
|
||||||
self._bg_last_activity = time.time()
|
self._user_last_activity = time.time()
|
||||||
return result
|
return result
|
||||||
except Exception:
|
except Exception as e2:
|
||||||
pass
|
raise TemporaryError(f"User batch call failed: {e2}")
|
||||||
|
|
||||||
return self.call_batch(requests, timeout)
|
raise TemporaryError(f"User batch call failed: {e}")
|
||||||
finally:
|
finally:
|
||||||
self._bg_lock.release()
|
self._user_lock.release()
|
||||||
|
|
||||||
def disconnect(self):
|
def disconnect(self):
|
||||||
self._stop_keepalive()
|
self._stop_keepalive()
|
||||||
with self._lock:
|
lock_acquired = self._lock.acquire(timeout=5)
|
||||||
if self._connection:
|
if lock_acquired:
|
||||||
self._connection.disconnect()
|
try:
|
||||||
self._connection = None
|
if self._connection:
|
||||||
with self._bg_lock:
|
self._connection.disconnect()
|
||||||
if self._bg_connection:
|
self._connection = None
|
||||||
|
finally:
|
||||||
|
self._lock.release()
|
||||||
|
else:
|
||||||
|
conn = self._connection
|
||||||
|
if conn:
|
||||||
try:
|
try:
|
||||||
self._bg_connection.disconnect()
|
conn.disconnect()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
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):
|
def get_balance(self, scripthash):
|
||||||
result = self.call("blockchain.scripthash.get_balance", [scripthash])
|
result = self.call("blockchain.scripthash.get_balance", [scripthash])
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# Copyright (c) 2022-2023 tecnovert
|
# 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
|
# Distributed under the MIT software license, see the accompanying
|
||||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||||
|
|
||||||
@@ -64,7 +64,7 @@ class FIROInterface(BTCInterface):
|
|||||||
# Firo shuts down after encryptwallet
|
# Firo shuts down after encryptwallet
|
||||||
seed_id_before: str = self.getWalletSeedID() if check_seed else "Not found"
|
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":
|
if check_seed is False or seed_id_before == "Not found":
|
||||||
return
|
return
|
||||||
@@ -102,6 +102,100 @@ class FIROInterface(BTCInterface):
|
|||||||
return addr_info["ismine"]
|
return addr_info["ismine"]
|
||||||
return addr_info["ismine"] or addr_info["iswatchonly"]
|
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:
|
def getSCLockScriptAddress(self, lock_script: bytes) -> str:
|
||||||
lock_tx_dest = self.getScriptDest(lock_script)
|
lock_tx_dest = self.getScriptDest(lock_script)
|
||||||
address = self.encodeScriptDest(lock_tx_dest)
|
address = self.encodeScriptDest(lock_tx_dest)
|
||||||
@@ -252,10 +346,6 @@ class FIROInterface(BTCInterface):
|
|||||||
assert len(script_hash) == 20
|
assert len(script_hash) == 20
|
||||||
return CScript([OP_HASH160, script_hash, OP_EQUAL])
|
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):
|
def getWalletSeedID(self):
|
||||||
return self.rpc("getwalletinfo")["hdmasterkeyid"]
|
return self.rpc("getwalletinfo")["hdmasterkeyid"]
|
||||||
|
|
||||||
|
|||||||
@@ -209,11 +209,9 @@ class LTCInterface(BTCInterface):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
seed_id = self.getWalletSeedID()
|
seed_id = self.getWalletSeedID()
|
||||||
self._log.debug(f"LTC unlockWallet getWalletSeedID returned: {seed_id}")
|
|
||||||
needs_seed_init = seed_id == "Not found"
|
needs_seed_init = seed_id == "Not found"
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
self._log.debug(f"getWalletSeedID failed: {e}")
|
||||||
self._log.debug(f"getWalletSeedID failed: {e}, will initialize seed")
|
|
||||||
needs_seed_init = True
|
needs_seed_init = True
|
||||||
if needs_seed_init:
|
if needs_seed_init:
|
||||||
self._log.info(f"Initializing HD seed for {self.coin_name()}.")
|
self._log.info(f"Initializing HD seed for {self.coin_name()}.")
|
||||||
@@ -221,7 +219,7 @@ class LTCInterface(BTCInterface):
|
|||||||
if password:
|
if password:
|
||||||
self._log.info(f"Encrypting {self.coin_name()} wallet.")
|
self._log.info(f"Encrypting {self.coin_name()} wallet.")
|
||||||
try:
|
try:
|
||||||
self.rpc_wallet("encryptwallet", [password])
|
self.rpc_wallet("encryptwallet", [password], timeout=120)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self._log.debug(f"encryptwallet returned: {e}")
|
self._log.debug(f"encryptwallet returned: {e}")
|
||||||
import time
|
import time
|
||||||
@@ -242,7 +240,7 @@ class LTCInterface(BTCInterface):
|
|||||||
check_seed = False
|
check_seed = False
|
||||||
|
|
||||||
if self.isWalletEncrypted():
|
if self.isWalletEncrypted():
|
||||||
self.rpc_wallet("walletpassphrase", [password, 100000000])
|
self.rpc_wallet("walletpassphrase", [password, 100000000], timeout=120)
|
||||||
|
|
||||||
if check_seed:
|
if check_seed:
|
||||||
self._sc.checkWalletSeed(self.coin_type())
|
self._sc.checkWalletSeed(self.coin_type())
|
||||||
@@ -332,7 +330,7 @@ class LTCInterfaceMWEB(LTCInterface):
|
|||||||
|
|
||||||
if password is not None:
|
if password is not None:
|
||||||
# Max timeout value, ~3 years
|
# Max timeout value, ~3 years
|
||||||
self.rpc_wallet("walletpassphrase", [password, 100000000])
|
self.rpc_wallet("walletpassphrase", [password, 100000000], timeout=120)
|
||||||
|
|
||||||
if self.getWalletSeedID() == "Not found":
|
if self.getWalletSeedID() == "Not found":
|
||||||
self._sc.initialiseWallet(self.interface_type())
|
self._sc.initialiseWallet(self.interface_type())
|
||||||
@@ -341,7 +339,7 @@ class LTCInterfaceMWEB(LTCInterface):
|
|||||||
self.rpc("unloadwallet", ["mweb"])
|
self.rpc("unloadwallet", ["mweb"])
|
||||||
self.rpc("loadwallet", ["mweb"])
|
self.rpc("loadwallet", ["mweb"])
|
||||||
if password is not None:
|
if password is not None:
|
||||||
self.rpc_wallet("walletpassphrase", [password, 100000000])
|
self.rpc_wallet("walletpassphrase", [password, 100000000], timeout=120)
|
||||||
self.rpc_wallet("keypoolrefill")
|
self.rpc_wallet("keypoolrefill")
|
||||||
|
|
||||||
def unlockWallet(self, password: str, check_seed: bool = True) -> None:
|
def unlockWallet(self, password: str, check_seed: bool = True) -> None:
|
||||||
@@ -355,15 +353,12 @@ class LTCInterfaceMWEB(LTCInterface):
|
|||||||
if not self.has_mweb_wallet():
|
if not self.has_mweb_wallet():
|
||||||
self.init_wallet(password)
|
self.init_wallet(password)
|
||||||
else:
|
else:
|
||||||
self.rpc_wallet("walletpassphrase", [password, 100000000])
|
self.rpc_wallet("walletpassphrase", [password, 100000000], timeout=120)
|
||||||
try:
|
try:
|
||||||
seed_id = self.getWalletSeedID()
|
seed_id = self.getWalletSeedID()
|
||||||
self._log.debug(
|
|
||||||
f"LTC_MWEB unlockWallet getWalletSeedID returned: {seed_id}"
|
|
||||||
)
|
|
||||||
needs_seed_init = seed_id == "Not found"
|
needs_seed_init = seed_id == "Not found"
|
||||||
except Exception as e:
|
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
|
needs_seed_init = True
|
||||||
if needs_seed_init:
|
if needs_seed_init:
|
||||||
self._log.info(f"Initializing HD seed for {self.coin_name()}.")
|
self._log.info(f"Initializing HD seed for {self.coin_name()}.")
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ class PIVXInterface(BTCInterface):
|
|||||||
|
|
||||||
seed_id_before: str = self.getWalletSeedID()
|
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":
|
if check_seed is False or seed_id_before == "Not found":
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -79,9 +79,11 @@ def withdraw_coin(swap_client, coin_type, post_string, is_json):
|
|||||||
txid_hex = swap_client.withdrawParticl(
|
txid_hex = swap_client.withdrawParticl(
|
||||||
type_from, type_to, value, address, subfee
|
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")
|
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):
|
elif coin_type in (Coins.XMR, Coins.WOW):
|
||||||
txid_hex = swap_client.withdrawCoin(coin_type, value, address, sweepall)
|
txid_hex = swap_client.withdrawCoin(coin_type, value, address, sweepall)
|
||||||
else:
|
else:
|
||||||
@@ -181,6 +183,15 @@ def js_walletbalances(self, url_split, post_string, is_json) -> bytes:
|
|||||||
version = ci.getDaemonVersion()
|
version = ci.getDaemonVersion()
|
||||||
if version:
|
if version:
|
||||||
coin_entry["version"] = 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)
|
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}"
|
wallet_seed_id = f"Error: {e}"
|
||||||
rv.update(
|
rv.update(
|
||||||
{
|
{
|
||||||
"seed": seed_key.hex(),
|
|
||||||
"seed_id": seed_id.hex(),
|
"seed_id": seed_id.hex(),
|
||||||
"expected_seed_id": "Unset" if expect_seedid is None else expect_seedid,
|
"expected_seed_id": "Unset" if expect_seedid is None else expect_seedid,
|
||||||
"current_seed_id": wallet_seed_id,
|
"current_seed_id": wallet_seed_id,
|
||||||
@@ -1739,38 +1749,57 @@ def js_modeswitchinfo(self, url_split, post_string, is_json) -> bytes:
|
|||||||
}
|
}
|
||||||
|
|
||||||
if direction == "lite":
|
if direction == "lite":
|
||||||
legacy_balance_sats = 0
|
non_bip84_balance_sats = 0
|
||||||
has_legacy_funds = False
|
has_non_bip84_funds = False
|
||||||
try:
|
try:
|
||||||
if hasattr(ci, "rpc_wallet"):
|
if hasattr(ci, "rpc_wallet"):
|
||||||
unspent = ci.rpc_wallet("listunspent")
|
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["show_transfer_option"] = True
|
||||||
rv["require_transfer"] = True
|
rv["require_transfer"] = True
|
||||||
rv["legacy_balance_sats"] = legacy_balance_sats
|
rv["legacy_balance_sats"] = non_bip84_balance_sats
|
||||||
rv["legacy_balance"] = ci.format_amount(legacy_balance_sats)
|
rv["legacy_balance"] = ci.format_amount(non_bip84_balance_sats)
|
||||||
rv["message"] = (
|
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:
|
else:
|
||||||
rv["show_transfer_option"] = False
|
rv["show_transfer_option"] = False
|
||||||
rv["require_transfer"] = False
|
rv["require_transfer"] = False
|
||||||
if has_legacy_funds:
|
if has_non_bip84_funds:
|
||||||
rv["legacy_balance_sats"] = legacy_balance_sats
|
rv["legacy_balance_sats"] = non_bip84_balance_sats
|
||||||
rv["legacy_balance"] = ci.format_amount(legacy_balance_sats)
|
rv["legacy_balance"] = ci.format_amount(non_bip84_balance_sats)
|
||||||
rv["message"] = "Legacy balance too low to transfer"
|
rv["message"] = "Non-derivable balance too low to transfer"
|
||||||
else:
|
else:
|
||||||
rv["legacy_balance_sats"] = 0
|
rv["legacy_balance_sats"] = 0
|
||||||
rv["legacy_balance"] = "0"
|
rv["legacy_balance"] = "0"
|
||||||
rv["message"] = "All funds on native segwit addresses"
|
rv["message"] = "All funds on BIP84 addresses"
|
||||||
else:
|
else:
|
||||||
rv["show_transfer_option"] = can_transfer
|
rv["show_transfer_option"] = can_transfer
|
||||||
if balance_sats == 0:
|
if balance_sats == 0:
|
||||||
|
|||||||
@@ -152,15 +152,17 @@ class Jsonrpc:
|
|||||||
pass
|
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:
|
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:
|
try:
|
||||||
url = "http://{}@{}:{}/".format(auth, host, rpc_port)
|
url = "http://{}@{}:{}/".format(auth, host, rpc_port)
|
||||||
if wallet is not None:
|
if wallet is not None:
|
||||||
url += "wallet/" + urllib.parse.quote(wallet)
|
url += "wallet/" + urllib.parse.quote(wallet)
|
||||||
x = Jsonrpc(url)
|
x = Jsonrpc(url, timeout=timeout if timeout else 10)
|
||||||
|
|
||||||
v = x.json_request(method, params)
|
v = x.json_request(method, params)
|
||||||
x.close()
|
x.close()
|
||||||
@@ -174,7 +176,9 @@ def callrpc(rpc_port, auth, method, params=[], wallet=None, host="127.0.0.1"):
|
|||||||
return r["result"]
|
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
|
from .rpc_pool import get_rpc_pool
|
||||||
import http.client
|
import http.client
|
||||||
import socket
|
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:
|
if wallet is not None:
|
||||||
url += "wallet/" + urllib.parse.quote(wallet)
|
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)
|
max_connections = _rpc_pool_settings.get("max_connections_per_daemon", 5)
|
||||||
pool = get_rpc_pool(url, max_connections)
|
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
|
wallet = wallet
|
||||||
host = host
|
host = host
|
||||||
|
|
||||||
def rpc_func(method, params=None, wallet_override=None):
|
def rpc_func(method, params=None, wallet_override=None, timeout=None):
|
||||||
return callrpc(
|
return callrpc(
|
||||||
port,
|
port,
|
||||||
auth,
|
auth,
|
||||||
@@ -255,6 +273,7 @@ def make_rpc_func(port, auth, wallet=None, host="127.0.0.1"):
|
|||||||
params,
|
params,
|
||||||
wallet if wallet_override is None else wallet_override,
|
wallet if wallet_override is None else wallet_override,
|
||||||
host,
|
host,
|
||||||
|
timeout=timeout,
|
||||||
)
|
)
|
||||||
|
|
||||||
return rpc_func
|
return rpc_func
|
||||||
|
|||||||
@@ -610,7 +610,7 @@ function ensureToastContainer() {
|
|||||||
clickAction = `onclick="window.location.href='/bid/${options.bidId}'"`;
|
clickAction = `onclick="window.location.href='/bid/${options.bidId}'"`;
|
||||||
cursorStyle = 'cursor-pointer';
|
cursorStyle = 'cursor-pointer';
|
||||||
} else if (options.coinSymbol) {
|
} else if (options.coinSymbol) {
|
||||||
clickAction = `onclick="window.location.href='/wallet/${options.coinSymbol}'"`;
|
clickAction = `onclick="window.location.href='/wallet/${options.coinSymbol.toLowerCase()}'"`;
|
||||||
cursorStyle = 'cursor-pointer';
|
cursorStyle = 'cursor-pointer';
|
||||||
} else if (options.releaseUrl) {
|
} else if (options.releaseUrl) {
|
||||||
clickAction = `onclick="window.open('${options.releaseUrl}', '_blank')"`;
|
clickAction = `onclick="window.open('${options.releaseUrl}', '_blank')"`;
|
||||||
@@ -739,9 +739,10 @@ function ensureToastContainer() {
|
|||||||
case 'sweep_completed':
|
case 'sweep_completed':
|
||||||
const sweepAmount = parseFloat(data.amount || 0).toFixed(8).replace(/\.?0+$/, '');
|
const sweepAmount = parseFloat(data.amount || 0).toFixed(8).replace(/\.?0+$/, '');
|
||||||
const sweepFee = parseFloat(data.fee || 0).toFixed(8).replace(/\.?0+$/, '');
|
const sweepFee = parseFloat(data.fee || 0).toFixed(8).replace(/\.?0+$/, '');
|
||||||
toastTitle = `Swept ${sweepAmount} ${data.coin_name} to RPC wallet`;
|
const sweepTicker = data.ticker || data.coin_name;
|
||||||
toastOptions.subtitle = `Fee: ${sweepFee} ${data.coin_name} • TXID: ${(data.txid || '').substring(0, 12)}...`;
|
toastTitle = `Swept ${sweepAmount} ${sweepTicker} to RPC wallet`;
|
||||||
toastOptions.coinSymbol = data.coin_name;
|
toastOptions.subtitle = `Fee: ${sweepFee} ${sweepTicker} • TXID: ${(data.txid || '').substring(0, 12)}...`;
|
||||||
|
toastOptions.coinSymbol = sweepTicker;
|
||||||
toastOptions.txid = data.txid;
|
toastOptions.txid = data.txid;
|
||||||
toastType = 'sweep_completed';
|
toastType = 'sweep_completed';
|
||||||
shouldShowToast = true;
|
shouldShowToast = true;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
const PAGE_SIZE = 50;
|
const PAGE_SIZE = 50;
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
dentities: new Map(),
|
identities: new Map(),
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
wsConnected: false,
|
wsConnected: false,
|
||||||
jsonData: [],
|
jsonData: [],
|
||||||
|
|||||||
@@ -251,15 +251,15 @@
|
|||||||
let transferSection = '';
|
let transferSection = '';
|
||||||
if (info.require_transfer && info.legacy_balance_sats > 0) {
|
if (info.require_transfer && info.legacy_balance_sats > 0) {
|
||||||
transferSection = `
|
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">
|
<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-yellow-800 dark:text-yellow-200 mb-2">Legacy Funds Transfer Required</p>
|
<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-300 mb-2">
|
<p class="text-xs text-gray-700 dark:text-gray-200 mb-2">
|
||||||
<strong>${info.legacy_balance} ${info.coin}</strong> on legacy addresses will be automatically transferred to a native segwit address.
|
<strong>${info.legacy_balance} ${info.coin}</strong> on non-derivable addresses will be automatically transferred to a BIP84 address.
|
||||||
</p>
|
</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}
|
Est. fee: ${info.estimated_fee} ${info.coin}
|
||||||
</p>
|
</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.
|
This ensures your funds are recoverable using the extended key backup in external Electrum wallets.
|
||||||
</p>
|
</p>
|
||||||
<input type="hidden" name="transfer_choice" value="auto">
|
<input type="hidden" name="transfer_choice" value="auto">
|
||||||
@@ -267,8 +267,8 @@
|
|||||||
`;
|
`;
|
||||||
} else if (info.legacy_balance_sats > 0 && !info.show_transfer_option) {
|
} else if (info.legacy_balance_sats > 0 && !info.show_transfer_option) {
|
||||||
transferSection = `
|
transferSection = `
|
||||||
<p class="text-yellow-700 dark:text-yellow-300 text-xs mb-3">
|
<p class="text-gray-700 dark:text-gray-300 text-xs mb-3">
|
||||||
Some funds on legacy addresses (${info.legacy_balance} ${info.coin}) - too low to transfer.
|
Some funds on non-derivable addresses (${info.legacy_balance} ${info.coin}) - too low to transfer.
|
||||||
</p>
|
</p>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -280,11 +280,22 @@
|
|||||||
</p>
|
</p>
|
||||||
<p class="mb-2 text-gray-800 dark:text-gray-100"><strong>Extended Private Key (for external wallet import):</strong></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">
|
<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>
|
</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}
|
${transferSection}
|
||||||
<div class="border-t border-gray-300 dark:border-gray-500 pt-3">
|
<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">
|
<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>
|
</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');
|
const checkbox = document.getElementById('walletModeKeyConfirmCheckbox');
|
||||||
if (checkbox) {
|
if (checkbox) {
|
||||||
checkbox.addEventListener('change', () => this.updateConfirmButtonState());
|
checkbox.addEventListener('change', () => this.updateConfirmButtonState());
|
||||||
@@ -362,21 +390,21 @@
|
|||||||
</p>
|
</p>
|
||||||
<div class="space-y-2">
|
<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">
|
<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>
|
<div>
|
||||||
<span class="text-sm font-medium text-gray-900 dark:text-white">Auto-transfer funds to RPC wallet</span>
|
<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>
|
<p class="text-xs text-gray-600 dark:text-gray-300">Recommended. Ensures all funds visible in full node wallet.</p>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
<label class="flex items-start cursor-pointer hover:bg-gray-300 dark:hover:bg-gray-600 rounded p-1.5 -m-1">
|
<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>
|
<div>
|
||||||
<span class="text-sm font-medium text-gray-900 dark:text-white">Keep funds on current addresses</span>
|
<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>
|
<p class="text-xs text-gray-600 dark:text-gray-300">Transfer manually later if needed.</p>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</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.
|
If you skip transfer, you will need to manually send funds from lite wallet addresses to your RPC wallet.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -87,7 +87,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (coinData.scan_status) {
|
if (coinData.scan_status || coinData.electrum_synced !== undefined) {
|
||||||
this.updateScanStatus(coinData);
|
this.updateScanStatus(coinData);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,7 +110,7 @@
|
|||||||
if (!scanStatusEl) return;
|
if (!scanStatusEl) return;
|
||||||
|
|
||||||
const status = coinData.scan_status;
|
const status = coinData.scan_status;
|
||||||
if (status.in_progress) {
|
if (status && status.in_progress) {
|
||||||
scanStatusEl.innerHTML = `
|
scanStatusEl.innerHTML = `
|
||||||
<div class="flex items-center justify-between text-xs">
|
<div class="flex items-center justify-between text-xs">
|
||||||
<span class="text-blue-600 dark:text-blue-300">
|
<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 class="bg-blue-600 dark:bg-blue-400 h-1 rounded-full transition-all" style="width: ${status.progress}%"></div>
|
||||||
</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 {
|
} else {
|
||||||
scanStatusEl.innerHTML = `
|
scanStatusEl.innerHTML = `
|
||||||
<div class="flex items-center text-xs text-green-600 dark:text-green-400">
|
<div class="bg-green-50 dark:bg-gray-500 p-2 rounded">
|
||||||
<svg class="inline-block w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<div class="flex items-center text-xs text-green-600 dark:text-green-400">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
Electrum Wallet Synced
|
||||||
</svg>
|
</div>
|
||||||
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 class="bg-gray-50 dark:bg-gray-700 rounded-lg p-4 space-y-4">
|
||||||
|
|
||||||
<div>
|
<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">
|
<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>• No blockchain download needed - connect via external Electrum servers</li>
|
||||||
<li>• Light wallet uses your seed to access existing funds</li>
|
<li>• Uses BIP84 derivation (native SegWit) - lower fees, modern addresses (bc1q.../ltc1q...)</li>
|
||||||
<li>• No transfer needed - same seed, same funds</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>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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">
|
<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>• Maximum privacy - no external servers, your node validates everything</li>
|
||||||
<li>• Any funds you RECEIVE go to these new addresses</li>
|
<li>• More wallet features: coin control, RBF, CPFP, raw transactions</li>
|
||||||
<li>• Your full node doesn't know about these addresses</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>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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">
|
<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>• To Light: Save your BIP84 key shown during switch (for external wallet import)</li>
|
||||||
<li>• These funds must be SENT back to your node wallet (real transaction, network fee applies based on current rate)</li>
|
<li>• To Full Node: Funds on light wallet addresses must be transferred (network fee applies)</li>
|
||||||
<li>• Enable "Auto-transfer" in Fund Transfer section to do this automatically on unlock</li>
|
<li>• Both modes share the same seed - switching is safe, just save keys when shown</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -259,13 +261,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="fund-transfer-section-{{ c.name }}" class="mb-6 hidden">
|
<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">
|
<h4 class="text-sm font-medium text-gray-900 dark:text-white mb-4">
|
||||||
Fund Transfer
|
Pending Balance
|
||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
<div class="bg-gray-50 dark:bg-gray-700 rounded-lg p-4">
|
<div class="bg-gray-50 dark:bg-gray-700 rounded-lg p-4">
|
||||||
{% if c.lite_wallet_balance %}
|
<div class="p-3 bg-orange-50 dark:bg-orange-900/30 border border-orange-200 dark:border-orange-700 rounded-lg">
|
||||||
<div class="mb-4 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">
|
<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>
|
<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
|
Light Wallet Balance Detected
|
||||||
@@ -292,24 +293,24 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
</div>
|
||||||
<label class="flex items-center cursor-pointer">
|
{% endif %}
|
||||||
<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>
|
|
||||||
|
|
||||||
{% if general_settings.debug %}
|
{% if general_settings.debug %}
|
||||||
<div class="mt-4 pt-4 border-t border-gray-200 dark:border-gray-600">
|
<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>
|
<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">
|
<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>
|
<span class="ml-2 text-xs text-gray-500 dark:text-gray-400">(5-100)</span>
|
||||||
</div>
|
</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>
|
<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>
|
</div>
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -864,7 +865,7 @@
|
|||||||
Switch Mode
|
Switch Mode
|
||||||
</button>
|
</button>
|
||||||
<button type="button" id="walletModeCancel"
|
<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
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -35,6 +35,8 @@
|
|||||||
</section>
|
</section>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{% if w.havedata %}
|
{% if w.havedata %}
|
||||||
{% if w.error %}
|
{% if w.error %}
|
||||||
<section class="py-4 px-6" id="messages_error" role="alert">
|
<section class="py-4 px-6" id="messages_error" role="alert">
|
||||||
@@ -183,8 +185,20 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</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 %}
|
{% endif %}
|
||||||
{# / LTC #}
|
{# / LTC #}
|
||||||
|
{# / FIRO #}
|
||||||
{% if w.locked_utxos %}
|
{% if w.locked_utxos %}
|
||||||
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
|
<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>
|
<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 bold">Synced:</td>
|
||||||
<td class="py-3 px-6">
|
<td class="py-3 px-6">
|
||||||
{% if is_electrum_mode %}
|
{% 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 %}
|
{% else %}
|
||||||
{{ w.synced }}
|
{{ w.synced }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -394,8 +412,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% if w.cid in '1, 3, 6, 9' %}
|
{% if w.cid in '1, 3, 6, 9, 13' %}
|
||||||
{# PART | LTC | XMR | WOW | #}
|
{# PART | LTC | XMR | WOW | FIRO #}
|
||||||
<div class="w-full md:w-1/2 p-3 flex justify-center items-center">
|
<div class="w-full md:w-1/2 p-3 flex justify-center items-center">
|
||||||
<div class="h-full">
|
<div class="h-full">
|
||||||
<div class="flex flex-wrap -m-3">
|
<div class="flex flex-wrap -m-3">
|
||||||
@@ -444,6 +462,22 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{# / LTC #}
|
{# / 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 %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -507,6 +541,15 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</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' %}
|
{% elif w.cid == '1' %}
|
||||||
{# PART #}
|
{# PART #}
|
||||||
<tr class="opacity-100 text-gray-500 dark:text-gray-100">
|
<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>
|
<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 #}
|
{# / 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 %}
|
{% 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 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>
|
<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>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</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 %}
|
{% endif %}
|
||||||
{# / LTC #}
|
{# / LTC #}
|
||||||
|
{# / FIRO #}
|
||||||
{% if w.cid not in '6,9' %} {# Not XMR WOW #}
|
{% if w.cid not in '6,9' %} {# Not XMR WOW #}
|
||||||
<tr class="opacity-100 text-gray-500 dark:text-gray-100">
|
<tr class="opacity-100 text-gray-500 dark:text-gray-100">
|
||||||
<td class="py-3 px-6 bold">Fee Rate:</td>
|
<td class="py-3 px-6 bold">Fee Rate:</td>
|
||||||
|
|||||||
@@ -148,6 +148,28 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{# / LTC #}
|
{# / 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">
|
<hr class="border-t border-gray-100 dark:border-gray-500 my-5">
|
||||||
<div class="flex mb-2 justify-between items-center">
|
<div class="flex mb-2 justify-between items-center">
|
||||||
<h4 class="text-xs font-medium dark:text-white">Blocks:</h4>
|
<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 class="bg-blue-600 dark:bg-blue-400 h-1 rounded-full" style="width: {{ w.scan_status.progress }}%"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% elif w.electrum_synced %}
|
||||||
<div class="bg-green-50 dark:bg-gray-500 p-2 rounded">
|
<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">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -189,7 +189,7 @@ def page_settings(self, url_split, post_string):
|
|||||||
)
|
)
|
||||||
data["auto_transfer_now"] = transfer_value == "true"
|
data["auto_transfer_now"] = transfer_value == "true"
|
||||||
gap_limit_str = get_data_entry_or(
|
gap_limit_str = get_data_entry_or(
|
||||||
form_data, "gap_limit_" + name, "20"
|
form_data, "gap_limit_" + name, "50"
|
||||||
).strip()
|
).strip()
|
||||||
try:
|
try:
|
||||||
gap_limit = int(gap_limit_str)
|
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,
|
"supports_electrum": name in electrum_supported_coins,
|
||||||
"clearnet_servers_text": clearnet_text,
|
"clearnet_servers_text": clearnet_text,
|
||||||
"onion_servers_text": onion_text,
|
"onion_servers_text": onion_text,
|
||||||
"address_gap_limit": c.get("address_gap_limit", 20),
|
"address_gap_limit": c.get("address_gap_limit", 50),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if name in ("monero", "wownero"):
|
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_address"] = w.get("mweb_address", "?")
|
||||||
wf["mweb_balance"] = w.get("mweb_balance", "?")
|
wf["mweb_balance"] = w.get("mweb_balance", "?")
|
||||||
wf["mweb_pending"] = w.get("mweb_pending", "?")
|
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"):
|
if hasattr(ci, "getScanStatus"):
|
||||||
wf["scan_status"] = ci.getScanStatus()
|
wf["scan_status"] = ci.getScanStatus()
|
||||||
@@ -114,6 +118,13 @@ def format_wallet_data(swap_client, ci, w):
|
|||||||
except Exception:
|
except Exception:
|
||||||
wf["electrum_connected"] = False
|
wf["electrum_connected"] = False
|
||||||
wf["electrum_status"] = "error"
|
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)
|
checkAddressesOwned(swap_client, ci, wf)
|
||||||
return wf
|
return wf
|
||||||
@@ -264,6 +275,8 @@ def page_wallet(self, url_split, post_string):
|
|||||||
force_refresh = True
|
force_refresh = True
|
||||||
elif have_data_entry(form_data, "newmwebaddr_" + cid):
|
elif have_data_entry(form_data, "newmwebaddr_" + cid):
|
||||||
swap_client.cacheNewStealthAddressForCoin(coin_id)
|
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):
|
elif have_data_entry(form_data, "reseed_" + cid):
|
||||||
try:
|
try:
|
||||||
swap_client.reseedWallet(coin_id)
|
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
|
page_data["wd_type_to_" + cid] = type_to
|
||||||
except Exception as e: # noqa: F841
|
except Exception as e: # noqa: F841
|
||||||
err_messages.append("Missing type")
|
err_messages.append("Missing type")
|
||||||
elif coin_id == Coins.LTC:
|
elif coin_id in (Coins.LTC, Coins.FIRO):
|
||||||
try:
|
try:
|
||||||
type_from = form_data[bytes("withdraw_type_from_" + cid, "utf-8")][
|
type_from = form_data[bytes("withdraw_type_from_" + cid, "utf-8")][
|
||||||
0
|
0
|
||||||
@@ -354,9 +367,9 @@ def page_wallet(self, url_split, post_string):
|
|||||||
value, ticker, type_from, type_to, address, txid
|
value, ticker, type_from, type_to, address, txid
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
elif coin_id == Coins.LTC:
|
elif coin_id in (Coins.LTC, Coins.FIRO):
|
||||||
txid = swap_client.withdrawLTC(
|
txid = swap_client.withdrawCoinExtended(
|
||||||
type_from, value, address, subfee
|
coin_id, type_from, value, address, subfee
|
||||||
)
|
)
|
||||||
messages.append(
|
messages.append(
|
||||||
"Withdrew {} {} (from {}) to address {}<br/>In txid: {}".format(
|
"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:
|
if swap_client.debug is True:
|
||||||
swap_client.log.error(traceback.format_exc())
|
swap_client.log.error(traceback.format_exc())
|
||||||
|
|
||||||
|
is_electrum_mode = (
|
||||||
|
swap_client.coin_clients.get(coin_id, {}).get("connection_type") == "electrum"
|
||||||
|
)
|
||||||
|
|
||||||
swap_client.updateWalletsInfo(
|
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})
|
wallets = swap_client.getCachedWalletsInfo({"coin_id": coin_id})
|
||||||
wallet_data = {}
|
wallet_data = {}
|
||||||
@@ -469,6 +486,8 @@ def page_wallet(self, url_split, post_string):
|
|||||||
wallet_data["main_address"] = w.get("main_address", "Refresh necessary")
|
wallet_data["main_address"] = w.get("main_address", "Refresh necessary")
|
||||||
elif k == Coins.LTC:
|
elif k == Coins.LTC:
|
||||||
wallet_data["mweb_address"] = w.get("mweb_address", "Refresh necessary")
|
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:
|
if "wd_type_from_" + cid in page_data:
|
||||||
wallet_data["wd_type_from"] = page_data["wd_type_from_" + cid]
|
wallet_data["wd_type_from"] = page_data["wd_type_from_" + cid]
|
||||||
|
|||||||
@@ -197,7 +197,11 @@ class ElectrumBackend(WalletBackend):
|
|||||||
self._cached_height_time = 0
|
self._cached_height_time = 0
|
||||||
self._height_cache_ttl = 5
|
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
|
self._background_mode = False
|
||||||
|
|
||||||
def setBackgroundMode(self, enabled: bool):
|
def setBackgroundMode(self, enabled: bool):
|
||||||
@@ -206,13 +210,20 @@ class ElectrumBackend(WalletBackend):
|
|||||||
def _call(self, method: str, params: list = None, timeout: int = 10):
|
def _call(self, method: str, params: list = None, timeout: int = 10):
|
||||||
if self._background_mode and hasattr(self._server, "call_background"):
|
if self._background_mode and hasattr(self._server, "call_background"):
|
||||||
return self._server.call_background(method, params, timeout)
|
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)
|
return self._server.call(method, params, timeout)
|
||||||
|
|
||||||
def _call_batch(self, calls: list, timeout: int = 15):
|
def _call_batch(self, calls: list, timeout: int = 15):
|
||||||
if self._background_mode and hasattr(self._server, "call_batch_background"):
|
if self._background_mode and hasattr(self._server, "call_batch_background"):
|
||||||
return self._server.call_batch_background(calls, timeout)
|
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)
|
return self._server.call_batch(calls, timeout)
|
||||||
|
|
||||||
|
def _is_server_stopping(self) -> bool:
|
||||||
|
return getattr(self._server, "_stopping", False)
|
||||||
|
|
||||||
def _split_batch_call(
|
def _split_batch_call(
|
||||||
self, scripthashes: list, method: str, batch_size: int = None
|
self, scripthashes: list, method: str, batch_size: int = None
|
||||||
) -> list:
|
) -> list:
|
||||||
@@ -221,19 +232,30 @@ class ElectrumBackend(WalletBackend):
|
|||||||
|
|
||||||
all_results = []
|
all_results = []
|
||||||
for i in range(0, len(scripthashes), batch_size):
|
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]
|
chunk = scripthashes[i : i + batch_size]
|
||||||
try:
|
try:
|
||||||
calls = [(method, [sh]) for sh in chunk]
|
calls = [(method, [sh]) for sh in chunk]
|
||||||
results = self._call_batch(calls)
|
results = self._call_batch(calls)
|
||||||
all_results.extend(results)
|
all_results.extend(results)
|
||||||
except Exception as e:
|
except Exception:
|
||||||
self._log.debug(f"Batch chunk failed ({len(chunk)} items): {e}")
|
if self._is_server_stopping():
|
||||||
|
self._log.debug(
|
||||||
|
"_split_batch_call: server stopping after batch failure, aborting"
|
||||||
|
)
|
||||||
|
break
|
||||||
for sh in chunk:
|
for sh in chunk:
|
||||||
|
if self._is_server_stopping():
|
||||||
|
self._log.debug(
|
||||||
|
"_split_batch_call: server stopping during fallback, aborting"
|
||||||
|
)
|
||||||
|
break
|
||||||
try:
|
try:
|
||||||
result = self._call(method, [sh])
|
result = self._call(method, [sh])
|
||||||
all_results.append(result)
|
all_results.append(result)
|
||||||
except Exception as e2:
|
except Exception:
|
||||||
self._log.debug(f"Individual call failed for {sh[:8]}...: {e2}")
|
|
||||||
all_results.append(None)
|
all_results.append(None)
|
||||||
return all_results
|
return all_results
|
||||||
|
|
||||||
@@ -298,8 +320,10 @@ class ElectrumBackend(WalletBackend):
|
|||||||
if not addr_list:
|
if not addr_list:
|
||||||
return result
|
return result
|
||||||
|
|
||||||
batch_size = 10
|
batch_size = self._max_batch_size
|
||||||
for batch_start in range(0, len(addr_list), 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]
|
batch = addr_list[batch_start : batch_start + batch_size]
|
||||||
|
|
||||||
addr_to_scripthash = {}
|
addr_to_scripthash = {}
|
||||||
@@ -332,6 +356,8 @@ class ElectrumBackend(WalletBackend):
|
|||||||
batch_success = True
|
batch_success = True
|
||||||
break
|
break
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
if self._is_server_stopping():
|
||||||
|
break
|
||||||
if attempt == 0:
|
if attempt == 0:
|
||||||
self._log.debug(
|
self._log.debug(
|
||||||
f"Batch detailed balance query failed, reconnecting: {e}"
|
f"Batch detailed balance query failed, reconnecting: {e}"
|
||||||
@@ -348,6 +374,8 @@ class ElectrumBackend(WalletBackend):
|
|||||||
|
|
||||||
if not batch_success:
|
if not batch_success:
|
||||||
for addr, scripthash in addr_to_scripthash.items():
|
for addr, scripthash in addr_to_scripthash.items():
|
||||||
|
if self._is_server_stopping():
|
||||||
|
break
|
||||||
try:
|
try:
|
||||||
balance = self._call(
|
balance = self._call(
|
||||||
"blockchain.scripthash.get_balance", [scripthash]
|
"blockchain.scripthash.get_balance", [scripthash]
|
||||||
@@ -569,13 +597,22 @@ class ElectrumBackend(WalletBackend):
|
|||||||
return self._cached_height if self._cached_height > 0 else 0
|
return self._cached_height if self._cached_height > 0 else 0
|
||||||
|
|
||||||
def estimateFee(self, blocks: int = 6) -> int:
|
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:
|
try:
|
||||||
fee = self._call("blockchain.estimatefee", [blocks])
|
fee = self._call("blockchain.estimatefee", [blocks])
|
||||||
if fee and fee > 0:
|
if fee and fee > 0:
|
||||||
return int(fee * 1e8 / 1000)
|
result = int(fee * 1e8 / 1000)
|
||||||
return 1
|
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:
|
except Exception:
|
||||||
return 1
|
return self._cached_fee.get(cache_key, 1)
|
||||||
|
|
||||||
def isConnected(self) -> bool:
|
def isConnected(self) -> bool:
|
||||||
try:
|
try:
|
||||||
@@ -616,6 +653,11 @@ class ElectrumBackend(WalletBackend):
|
|||||||
status["version"] = self.getServerVersion()
|
status["version"] = self.getServerVersion()
|
||||||
return status
|
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]:
|
def getAddressHistory(self, address: str) -> List[dict]:
|
||||||
if self._isUnsupportedAddress(address):
|
if self._isUnsupportedAddress(address):
|
||||||
return []
|
return []
|
||||||
@@ -751,5 +793,29 @@ class ElectrumBackend(WalletBackend):
|
|||||||
self._log.debug(f"Failed to subscribe to {address}: {e}")
|
self._log.debug(f"Failed to subscribe to {address}: {e}")
|
||||||
return None
|
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):
|
def getServer(self):
|
||||||
return self._server
|
return self._server
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ class WalletManager:
|
|||||||
Coins.LTC: {"mainnet": "ltc", "testnet": "tltc", "regtest": "rltc"},
|
Coins.LTC: {"mainnet": "ltc", "testnet": "tltc", "regtest": "rltc"},
|
||||||
}
|
}
|
||||||
|
|
||||||
GAP_LIMIT = 20
|
GAP_LIMIT = 50
|
||||||
|
|
||||||
def __init__(self, swap_client, log):
|
def __init__(self, swap_client, log):
|
||||||
self._gap_limits: Dict[Coins, int] = {}
|
self._gap_limits: Dict[Coins, int] = {}
|
||||||
@@ -51,6 +51,12 @@ class WalletManager:
|
|||||||
self._migration_in_progress: set = set()
|
self._migration_in_progress: set = set()
|
||||||
self._balance_sync_lock = threading.Lock()
|
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:
|
def initialize(self, coin_type: Coins, root_key) -> None:
|
||||||
if coin_type not in self.SUPPORTED_COINS:
|
if coin_type not in self.SUPPORTED_COINS:
|
||||||
raise ValueError(f"Coin {coin_type} not supported by WalletManager")
|
raise ValueError(f"Coin {coin_type} not supported by WalletManager")
|
||||||
|
|||||||
@@ -1489,7 +1489,11 @@ class Test(BaseTest):
|
|||||||
v = ci.getNewRandomKey()
|
v = ci.getNewRandomKey()
|
||||||
s = ci.getNewRandomKey()
|
s = ci.getNewRandomKey()
|
||||||
S = ci.getPubkey(s)
|
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)
|
test_delay_event.wait(1)
|
||||||
|
|
||||||
addr_out = ci.getNewAddress(True)
|
addr_out = ci.getNewAddress(True)
|
||||||
|
|||||||
@@ -412,7 +412,11 @@ class Test(TestFunctions):
|
|||||||
v = ci.getNewRandomKey()
|
v = ci.getNewRandomKey()
|
||||||
s = ci.getNewRandomKey()
|
s = ci.getNewRandomKey()
|
||||||
S = ci.getPubkey(s)
|
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)
|
test_delay_event.wait(1)
|
||||||
|
|
||||||
addr_out = ci.getNewAddress(False)
|
addr_out = ci.getNewAddress(False)
|
||||||
|
|||||||
@@ -1579,7 +1579,11 @@ class BasicSwapTest(TestFunctions):
|
|||||||
v = ci.getNewRandomKey()
|
v = ci.getNewRandomKey()
|
||||||
s = ci.getNewRandomKey()
|
s = ci.getNewRandomKey()
|
||||||
S = ci.getPubkey(s)
|
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)
|
addr_out = ci.getNewAddress(True)
|
||||||
lock_tx_b_spend_txid = ci.spendBLockTx(
|
lock_tx_b_spend_txid = ci.spendBLockTx(
|
||||||
|
|||||||
@@ -1218,7 +1218,11 @@ class Test(BaseTest):
|
|||||||
v = ci.getNewRandomKey()
|
v = ci.getNewRandomKey()
|
||||||
s = ci.getNewRandomKey()
|
s = ci.getNewRandomKey()
|
||||||
S = ci.getPubkey(s)
|
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)
|
addr_out = ci.getNewAddress(True)
|
||||||
lock_tx_b_spend_txid = ci.spendBLockTx(
|
lock_tx_b_spend_txid = ci.spendBLockTx(
|
||||||
@@ -1247,7 +1251,11 @@ class Test(BaseTest):
|
|||||||
v = ci.getNewRandomKey()
|
v = ci.getNewRandomKey()
|
||||||
s = ci.getNewRandomKey()
|
s = ci.getNewRandomKey()
|
||||||
S = ci.getPubkey(s)
|
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)
|
addr_out = ci.getNewAddress(True)
|
||||||
for i in range(20):
|
for i in range(20):
|
||||||
@@ -2306,7 +2314,11 @@ class Test(BaseTest):
|
|||||||
v = ci.getNewRandomKey()
|
v = ci.getNewRandomKey()
|
||||||
s = ci.getNewRandomKey()
|
s = ci.getNewRandomKey()
|
||||||
S = ci.getPubkey(s)
|
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()
|
addr_out = ci.getNewStealthAddress()
|
||||||
lock_tx_b_spend_txid = None
|
lock_tx_b_spend_txid = None
|
||||||
|
|||||||
Reference in New Issue
Block a user