From 7ee1cea4eb72b3904f5d094608065bf084954fc6 Mon Sep 17 00:00:00 2001 From: Dhaval Chaudhari Date: Thu, 20 Nov 2025 00:22:36 +0530 Subject: [PATCH] feat: implement Spark balance display and withdrawal options feat: complete FIRO + Spark integration (balance, withdrawal, address caching, refactor) feat: add support for Spark address handling remove white space ref --- basicswap/basicswap.py | 69 ++++++------------ basicswap/interface/firo.py | 73 +++++++++++--------- basicswap/js_server.py | 9 ++- basicswap/static/js/modules/wallet-amount.js | 19 ++++- basicswap/templates/wallet.html | 64 ++++++++++++++++- basicswap/templates/wallets.html | 22 ++++++ basicswap/ui/page_wallet.py | 27 ++------ 7 files changed, 174 insertions(+), 109 deletions(-) diff --git a/basicswap/basicswap.py b/basicswap/basicswap.py index e2bab28..3c6cdab 100644 --- a/basicswap/basicswap.py +++ b/basicswap/basicswap.py @@ -2966,25 +2966,12 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp): self.log.info_s(f"In txn: {txid}") return txid - def withdrawLTC(self, type_from, value, addr_to, subfee: bool) -> str: - ci = self.ci(Coins.LTC) + def withdrawCoinExtended( + self, coin_type, type_from, value, addr_to, subfee: bool + ) -> str: + ci = self.ci(coin_type) self.log.info( - "withdrawLTC{}".format( - "" - if self.log.safe_logs - else " {} {} to {} {}".format( - value, type_from, addr_to, " subfee" if subfee else "" - ) - ) - ) - txid = ci.withdrawCoin(value, type_from, addr_to, subfee) - self.log.info_s(f"In txn: {txid}") - return txid - - def withdrawFIRO(self, type_from, value, addr_to, subfee: bool) -> str: - ci = self.ci(Coins.FIRO) - self.log.info( - "withdrawFIRO{}".format( + "withdrawCoinExtended{}".format( "" if self.log.safe_logs else " {} {} to {} {}".format( @@ -3145,15 +3132,6 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp): self.setStringKV(key_str, addr) return addr - def cacheNewSparkAddressForCoin(self, coin_type): - """Cache a new Spark address for FIRO.""" - self.log.debug(f"cacheNewSparkAddressForCoin {Coins(coin_type).name}") - ci = self.ci(coin_type) - key_str = "spark_addr_" + ci.coin_name().lower() - addr = ci.getNewSparkAddress() - self.setStringKV(key_str, addr) - return addr - def getCachedStealthAddressForCoin(self, coin_type, cursor=None): self.log.debug(f"getCachedStealthAddressForCoin {Coins(coin_type).name}") @@ -3173,23 +3151,6 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp): self.closeDB(use_cursor) return addr - def getCachedSparkAddressForCoin(self, coin_type, cursor=None): - """Get cached Spark address for FIRO, generating one if needed.""" - self.log.debug(f"getCachedSparkAddressForCoin {Coins(coin_type).name}") - ci = self.ci(coin_type) - key_str = "spark_addr_" + ci.coin_name().lower() - use_cursor = self.openDB(cursor) - try: - addr = self.getStringKV(key_str, use_cursor) - if addr is None: - addr = ci.getNewSparkAddress() - self.log.info(f"Generated new Spark address for {ci.coin_name()}") - self.setStringKV(key_str, addr, use_cursor) - finally: - if cursor is None: - self.closeDB(use_cursor) - return addr - def getCachedWalletRestoreHeight(self, ci, cursor=None): self.log.debug(f"getCachedWalletRestoreHeight {ci.coin_name()}") @@ -11661,15 +11622,25 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp): ) elif coin == Coins.FIRO: try: - rv["spark_address"] = self.getCachedSparkAddressForCoin(Coins.FIRO) + rv["spark_address"] = self.getCachedStealthAddressForCoin( + Coins.FIRO + ) except Exception as e: self.log.warning( - f"getCachedSparkAddressForCoin for {ci.coin_name()} failed with: {e}." + f"getCachedStealthAddressForCoin for {ci.coin_name()} failed with: {e}." ) - rv["spark_balance"] = walletinfo["spark_balance"] - rv["spark_pending"] = ( + # Spark balances are in atomic units, format them + rv["spark_balance"] = ( + 0 + if walletinfo["spark_balance"] == 0 + else ci.format_amount(walletinfo["spark_balance"]) + ) + spark_pending_int = ( walletinfo["spark_unconfirmed"] + walletinfo["spark_immature"] ) + rv["spark_pending"] = ( + 0 if spark_pending_int == 0 else ci.format_amount(spark_pending_int) + ) return rv except Exception as e: @@ -11792,6 +11763,8 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp): if row2[0].startswith("stealth"): if coin_id == Coins.LTC: wallet_data["mweb_address"] = row2[1] + elif coin_id == Coins.FIRO: + wallet_data["spark_address"] = row2[1] else: wallet_data["stealth_address"] = row2[1] else: diff --git a/basicswap/interface/firo.py b/basicswap/interface/firo.py index e3e50e8..cb0ed85 100644 --- a/basicswap/interface/firo.py +++ b/basicswap/interface/firo.py @@ -102,30 +102,33 @@ class FIROInterface(BTCInterface): return addr_info["ismine"] return addr_info["ismine"] or addr_info["iswatchonly"] - def getNewSparkAddress(self, label="swap_receive") -> str: - """Generate a new Spark address for receiving private funds. - RPC: getnewsparkaddress [label] - """ + def getNewSparkAddress(self) -> str: try: - return self.rpc("getnewsparkaddress", [label]) + return self.rpc_wallet("getnewsparkaddress")[0] except Exception as e: self._log.error(f"getnewsparkaddress failed: {str(e)}") raise - def getNewStealthAddress(self, label=""): + def getNewStealthAddress(self): """Get a new Spark address (alias for consistency with other coins).""" - return self.getNewSparkAddress(label) + 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 a dict with confirmed, unconfirmed, immature - # Values are in FIRO (not satoshis), similar to getwalletinfo balance - rv["spark_balance"] = spark_balance_info.get("confirmed", 0) - rv["spark_unconfirmed"] = spark_balance_info.get("unconfirmed", 0) - rv["spark_immature"] = spark_balance_info.get("immature", 0) + # 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 @@ -135,36 +138,48 @@ class FIROInterface(BTCInterface): 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 """ - if type_from == "spark": - # Use spendspark RPC for Spark transactions - # RPC: spendspark {"address": {"amount": ..., "subtractfee": ..., "memo": ...}} + 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: - params = { - addr_to: { - "amount": value, - "subtractfee": subfee, - "memo": "" - } - } - result = self.rpc("spendspark", params) - # spendspark returns a txid string directly or in a result dict + 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") + print(f"balance {balance} value {value} subfee {subfee}") + if str(balance) == str(value) and subfee: + result = self.rpc_wallet("automintspark") + else: + # subtractFee param is not available on plain -> spark transactions + params = [{addr_to: {"amount": value}}] + 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"spendspark failed: {str(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("sendtoaddress", params) + return self.rpc_wallet("sendtoaddress", params) def getSCLockScriptAddress(self, lock_script: bytes) -> str: lock_tx_dest = self.getScriptDest(lock_script) @@ -316,10 +331,6 @@ class FIROInterface(BTCInterface): assert len(script_hash) == 20 return CScript([OP_HASH160, script_hash, OP_EQUAL]) - def withdrawCoin(self, value, addr_to, subfee): - params = [addr_to, value, "", "", subfee] - return self.rpc("sendtoaddress", params) - def getWalletSeedID(self): return self.rpc("getwalletinfo")["hdmasterkeyid"] diff --git a/basicswap/js_server.py b/basicswap/js_server.py index 7fb8148..21c80cf 100644 --- a/basicswap/js_server.py +++ b/basicswap/js_server.py @@ -79,12 +79,11 @@ def withdraw_coin(swap_client, coin_type, post_string, is_json): txid_hex = swap_client.withdrawParticl( type_from, type_to, value, address, subfee ) - elif coin_type == Coins.LTC: + elif coin_type in (Coins.LTC, Coins.FIRO): type_from = get_data_entry_or(post_data, "type_from", "plain") - txid_hex = swap_client.withdrawLTC(type_from, value, address, subfee) - elif coin_type == Coins.FIRO: - type_from = get_data_entry_or(post_data, "type_from", "plain") - txid_hex = swap_client.withdrawFIRO(type_from, value, address, subfee) + txid_hex = swap_client.withdrawCoinExtended( + coin_type, type_from, value, address, subfee + ) elif coin_type in (Coins.XMR, Coins.WOW): txid_hex = swap_client.withdrawCoin(coin_type, value, address, sweepall) else: diff --git a/basicswap/static/js/modules/wallet-amount.js b/basicswap/static/js/modules/wallet-amount.js index 3e14cb5..fbcf9df 100644 --- a/basicswap/static/js/modules/wallet-amount.js +++ b/basicswap/static/js/modules/wallet-amount.js @@ -23,6 +23,11 @@ types: ['default'], hasSubfee: false, hasSweepAll: true + }, + 13: { + types: ['plain', 'spark'], + hasSubfee: true, + hasSweepAll: false } }, @@ -64,6 +69,17 @@ } } + if (cid === 13) { + switch(selectedType) { + case 'plain': + return this.safeParseFloat(balances.main || balances.balance); + case 'spark': + return this.safeParseFloat(balances.spark); + default: + return this.safeParseFloat(balances.main || balances.balance); + } + } + return this.safeParseFloat(balances.main || balances.balance); }, @@ -188,7 +204,8 @@ balance: balance, blind: balance2, anon: balance3, - mweb: balance2 + mweb: balance2, + spark: balance2 }; WalletAmountManager.setAmount(percent, balances, coinId); }; diff --git a/basicswap/templates/wallet.html b/basicswap/templates/wallet.html index c4b61db..ed3f668 100644 --- a/basicswap/templates/wallet.html +++ b/basicswap/templates/wallet.html @@ -35,6 +35,8 @@ {% endif %} + + {% if w.havedata %} {% if w.error %}