diff --git a/basicswap/basicswap.py b/basicswap/basicswap.py index 26e7d45..1c81601 100644 --- a/basicswap/basicswap.py +++ b/basicswap/basicswap.py @@ -2955,10 +2955,12 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp): self.log.info_s(f"In txn: {txid}") return txid - def withdrawLTC(self, type_from, value, addr_to, subfee: bool) -> str: - ci = self.ci(Coins.LTC) + def withdrawCoinExtended( + self, coin_type, type_from, value, addr_to, subfee: bool + ) -> str: + ci = self.ci(coin_type) self.log.info( - "withdrawLTC{}".format( + "withdrawCoinExtended{}".format( "" if self.log.safe_logs else " {} {} to {} {}".format( @@ -11676,6 +11678,27 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp): rv["mweb_pending"] = ( walletinfo["mweb_unconfirmed"] + walletinfo["mweb_immature"] ) + 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}." + ) + # 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: @@ -11798,6 +11821,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 d536196..05dc755 100644 --- a/basicswap/interface/firo.py +++ b/basicswap/interface/firo.py @@ -102,6 +102,87 @@ class FIROInterface(BTCInterface): return addr_info["ismine"] return addr_info["ismine"] or addr_info["iswatchonly"] + def getNewSparkAddress(self) -> str: + try: + return self.rpc_wallet("getnewsparkaddress")[0] + except Exception as e: + self._log.error(f"getnewsparkaddress failed: {str(e)}") + raise + + def getNewStealthAddress(self): + """Get a new Spark address (alias for consistency with other coins).""" + return self.getNewSparkAddress() + + def getWalletInfo(self): + """Get wallet info including Spark balance.""" + rv = super(FIROInterface, self).getWalletInfo() + try: + spark_balance_info = self.rpc("getsparkbalance") + # getsparkbalance returns amounts in atomic units (satoshis) + # Field names: availableBalance, unconfirmedBalance, fullBalance + confirmed = spark_balance_info.get("availableBalance", 0) + unconfirmed = spark_balance_info.get("unconfirmedBalance", 0) + full_balance = spark_balance_info.get("fullBalance", 0) + # Values are already in atomic units, keep as integers + # basicswap.py will format them using format_amount + rv["spark_balance"] = confirmed if confirmed else 0 + rv["spark_unconfirmed"] = unconfirmed if unconfirmed else 0 + immature = full_balance - confirmed - unconfirmed + rv["spark_immature"] = immature if immature > 0 else 0 + except Exception as e: + self._log.warning(f"getsparkbalance failed: {str(e)}") + rv["spark_balance"] = 0 + rv["spark_unconfirmed"] = 0 + rv["spark_immature"] = 0 + return rv + + def withdrawCoin(self, value, type_from: str, addr_to: str, subfee: bool) -> str: + """Withdraw coins, supporting both transparent and Spark transactions. + + Args: + value: Amount to withdraw + type_from: "plain" for transparent, "spark" for Spark + addr_to: Destination address + subfee: Whether to subtract fee from amount + """ + type_to = "spark" if addr_to.startswith("sm1") else "plain" + + if "spark" in (type_from, type_to): + # RPC format: spendspark {"address": {"amount": ..., "subtractfee": ..., "memo": ...}} + # RPC wrapper will serialize this as: {"method": "spendspark", "params": [{...}], ...} + try: + if type_from == "spark": + # Construct params: dict where address is the key, wrapped in array for RPC + params = [ + {"address": addr_to, "amount": value, "subtractfee": subfee} + ] + result = self.rpc_wallet("spendspark", params) + else: + # Use automintspark to perform a plain -> spark tx of full balance + balance = self.rpc_wallet("getbalance") + if str(balance) == str(value): + result = self.rpc_wallet("automintspark") + else: + # subfee param is available on plain -> spark transactions + mint_params = {"amount": value} + if subfee: + mint_params["subfee"] = True + params = [{addr_to: mint_params}] + result = self.rpc_wallet("mintspark", params) + # spendspark returns a txid string directly, in a result dict, or as an array + if isinstance(result, list) and len(result) > 0: + return result[0] + if isinstance(result, dict): + return result.get("txid", result.get("tx", "")) + return result + except Exception as e: + self._log.error(f"spark tx failed: {str(e)}") + raise + else: + # Use standard sendtoaddress for transparent transactions + params = [addr_to, value, "", "", subfee] + return self.rpc_wallet("sendtoaddress", params) + def getSCLockScriptAddress(self, lock_script: bytes) -> str: lock_tx_dest = self.getScriptDest(lock_script) address = self.encodeScriptDest(lock_tx_dest) @@ -252,10 +333,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 1ecd597..21c80cf 100644 --- a/basicswap/js_server.py +++ b/basicswap/js_server.py @@ -79,9 +79,11 @@ def withdraw_coin(swap_client, coin_type, post_string, is_json): txid_hex = swap_client.withdrawParticl( type_from, type_to, value, address, subfee ) - elif coin_type == Coins.LTC: + elif coin_type in (Coins.LTC, Coins.FIRO): type_from = get_data_entry_or(post_data, "type_from", "plain") - txid_hex = swap_client.withdrawLTC(type_from, value, address, subfee) + txid_hex = swap_client.withdrawCoinExtended( + coin_type, type_from, value, address, subfee + ) elif coin_type in (Coins.XMR, Coins.WOW): txid_hex = swap_client.withdrawCoin(coin_type, value, address, sweepall) else: 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 %}