Merge pull request #406 from dhvll/firo-spark

Add Firo Spark Support
This commit is contained in:
tecnovert
2026-03-18 08:22:50 +00:00
committed by GitHub
7 changed files with 227 additions and 16 deletions

View File

@@ -2955,10 +2955,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(
@@ -11676,6 +11678,27 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
rv["mweb_pending"] = ( rv["mweb_pending"] = (
walletinfo["mweb_unconfirmed"] + walletinfo["mweb_immature"] 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 return rv
except Exception as e: except Exception as e:
@@ -11798,6 +11821,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:

View File

@@ -102,6 +102,87 @@ 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 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 +333,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"]

View File

@@ -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:

View File

@@ -23,6 +23,11 @@
types: ['default'], types: ['default'],
hasSubfee: false, hasSubfee: false,
hasSweepAll: true 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); return this.safeParseFloat(balances.main || balances.balance);
}, },
@@ -188,7 +204,8 @@
balance: balance, balance: balance,
blind: balance2, blind: balance2,
anon: balance3, anon: balance3,
mweb: balance2 mweb: balance2,
spark: balance2
}; };
WalletAmountManager.setAmount(percent, balances, coinId); WalletAmountManager.setAmount(percent, balances, coinId);
}; };

View File

@@ -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">
@@ -146,8 +148,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>
@@ -286,8 +300,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">
@@ -334,6 +348,22 @@
</div> </div>
</div> </div>
{# / 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>
@@ -397,6 +427,15 @@
(<span class="usd-value"></span>) (<span class="usd-value"></span>)
</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">
@@ -487,6 +526,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>
@@ -553,8 +600,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>

View File

@@ -132,6 +132,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>

View File

@@ -82,6 +82,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", "?")
checkAddressesOwned(swap_client, ci, wf) checkAddressesOwned(swap_client, ci, wf)
return wf return wf
@@ -163,6 +167,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)
@@ -208,7 +214,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
@@ -230,9 +236,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(
@@ -342,6 +348,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]