Merge pull request #466 from tecnovert/mweb_change_helper

LTC MWEB change back to LTC helper functions
This commit is contained in:
tecnovert
2026-05-06 18:24:11 +00:00
committed by GitHub
8 changed files with 201 additions and 7 deletions
+73 -1
View File
@@ -92,7 +92,9 @@ class LTCInterface(BTCInterface):
if "address" not in u: if "address" not in u:
continue continue
utxo_address: str = u["address"] utxo_address: str = u["address"]
if any(utxo_address.startswith(prefix) for prefix in ("mweb1", "tmweb1")): if any(
utxo_address.startswith(prefix) for prefix in ("ltcmweb1", "tmweb1")
):
continue continue
if "desc" in u: if "desc" in u:
desc = u["desc"] desc = u["desc"]
@@ -111,6 +113,76 @@ class LTCInterface(BTCInterface):
) + self.make_int(u["amount"], r=1) ) + self.make_int(u["amount"], r=1)
return unspent_addr return unspent_addr
def getMWEBBalance(self) -> int:
if self.useBackend():
raise ValueError("MWEB not supported in electrum mode")
value: int = 0
unspent = self.rpc_wallet(
"listunspent",
[
0,
],
)
for u in unspent:
if "address" not in u:
continue
utxo_address: str = u["address"]
if any(
utxo_address.startswith(prefix) for prefix in ("ltcmweb1", "tmweb1")
):
value += self.make_int(u["amount"], r=1)
return value
def convertMWEBBalance(self):
if self.useBackend():
raise ValueError("MWEB not supported in electrum mode")
self._log.info(f"convertMWEBBalance - {self.ticker()}")
locked_before = self.rpc_wallet("listlockunspent")
lock_utxos = []
try:
# Hack: mark all the other utxos as unspendable, alternative is to use a mweb_transfer wallet
utxos = self.rpc_wallet("listunspent")
mweb_amount: int = 0
for utxo in utxos:
utxo_address: str = utxo.get("address", "")
if any(
utxo_address.startswith(prefix) for prefix in ("ltcmweb1", "tmweb1")
):
mweb_amount += self.make_int(utxo["amount"], r=1)
continue
utxo_op = {"txid": utxo["txid"], "vout": utxo["vout"]}
if utxo_op in locked_before:
continue
lock_utxos.append(utxo_op)
if mweb_amount == 0:
raise ValueError("No MWEB outputs to convert")
self.rpc_wallet("lockunspent", [False, lock_utxos])
subfee_to_mweb: bool = True
convert_value = self.format_amount(mweb_amount)
plain_addr: str = self.rpc_wallet("getnewaddress", ["transfer", "bech32"])
# Double check generated address is owned by this wallet
if not self.isAddressMine(plain_addr):
raise ValueError("Generated address not owned by wallet!")
params = [
plain_addr,
convert_value,
"",
"",
subfee_to_mweb,
True,
self._conf_target,
]
txid = self.rpc_wallet("sendtoaddress", params)
self._log.info(f"MWEB in plain converted in txid: {self._log.id(txid)}")
return txid
finally:
self.rpc_wallet("lockunspent", [True, lock_utxos])
def unlockWallet(self, password: str, check_seed: bool = True) -> None: def unlockWallet(self, password: str, check_seed: bool = True) -> None:
if password == "": if password == "":
return return
+13 -2
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2020-2024 tecnovert # Copyright (c) 2020-2024 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.
@@ -129,7 +129,6 @@ def js_walletbalances(self, url_split, post_string, is_json) -> bytes:
swap_client = self.server.swap_client swap_client = self.server.swap_client
try: try:
swap_client.updateWalletsInfo() swap_client.updateWalletsInfo()
wallets = swap_client.getCachedWalletsInfo() wallets = swap_client.getCachedWalletsInfo()
coins_with_balances = [] coins_with_balances = []
@@ -332,6 +331,18 @@ def js_wallets(self, url_split, post_string, is_json):
return bytes( return bytes(
json.dumps(swap_client.ci(coin_type).getNewMwebAddress()), "UTF-8" json.dumps(swap_client.ci(coin_type).getNewMwebAddress()), "UTF-8"
) )
elif cmd == "mwebbalance":
# mweb outputs left behind when sending LTC -> MWEB
if coin_type not in (Coins.LTC,):
raise ValueError("Invalid coin for command")
ci = swap_client.ci(coin_type)
return bytes(json.dumps(ci.format_amount(ci.getMWEBBalance())), "UTF-8")
elif cmd == "convertmweb":
if coin_type not in (Coins.LTC,):
raise ValueError("Invalid coin for command")
return bytes(
json.dumps(swap_client.ci(coin_type).convertMWEBBalance()), "UTF-8"
)
elif cmd == "watchaddress": elif cmd == "watchaddress":
post_data = getFormData(post_string, is_json) post_data = getFormData(post_string, is_json)
address = get_data_entry(post_data, "address") address = get_data_entry(post_data, "address")
@@ -40,6 +40,10 @@
}); });
}, },
confirmMWEBChangeConvert: function() {
return confirm('Confirm MWEB change conversion: This will create a tx sending all spendable MWEB outputs in the plain LTC wallet to LTC.');
},
confirmReseed: function() { confirmReseed: function() {
return confirm('Are you sure you want to reseed the wallet? This will generate new addresses.'); return confirm('Are you sure you want to reseed the wallet? This will generate new addresses.');
}, },
@@ -282,6 +286,16 @@
} }
}); });
document.addEventListener('click', (e) => {
const target = e.target.closest('[data-confirm-mweb-change-convert]');
if (target) {
if (!this.confirmMWEBChangeConvert()) {
e.preventDefault();
return false;
}
}
});
document.addEventListener('click', (e) => { document.addEventListener('click', (e) => {
const target = e.target.closest('[data-confirm-utxo]'); const target = e.target.closest('[data-confirm-utxo]');
if (target) { if (target) {
@@ -398,6 +412,7 @@
window.EventHandlers = EventHandlers; window.EventHandlers = EventHandlers;
window.confirmReseed = EventHandlers.confirmReseed.bind(EventHandlers); window.confirmReseed = EventHandlers.confirmReseed.bind(EventHandlers);
window.confirmMWEBChangeConvert = EventHandlers.confirmMWEBChangeConvert.bind(EventHandlers);
window.confirmWithdrawal = EventHandlers.confirmWithdrawal.bind(EventHandlers); window.confirmWithdrawal = EventHandlers.confirmWithdrawal.bind(EventHandlers);
window.confirmUTXOResize = EventHandlers.confirmUTXOResize.bind(EventHandlers); window.confirmUTXOResize = EventHandlers.confirmUTXOResize.bind(EventHandlers);
window.confirmRemoveExpired = EventHandlers.confirmRemoveExpired.bind(EventHandlers); window.confirmRemoveExpired = EventHandlers.confirmRemoveExpired.bind(EventHandlers);
+11
View File
@@ -185,6 +185,17 @@
{% endif %} {% endif %}
</td> </td>
</tr> </tr>
{% if w.mweb_in_plain %}
<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 }} MWEB"> </span>MWEB in Plain Balance: </td>
<td class="py-3 px-6 bold">
<span>{{ w.mweb_in_plain }} {{ w.ticker }}</span>
</td>
<td class="py-3 px-6 bold">
<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="convertmweb_{{ w.cid }}" value="Convert" data-confirm-mweb-change-convert> Convert </button>
</td>
</tr>
{% endif %}
{% elif w.cid == '13' %} {# FIRO #} {% 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"> <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="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>
+7
View File
@@ -273,6 +273,9 @@ def page_wallet(self, url_split, post_string):
swap_client.cacheNewAddressForCoin(coin_id) swap_client.cacheNewAddressForCoin(coin_id)
elif have_data_entry(form_data, "forcerefresh"): elif have_data_entry(form_data, "forcerefresh"):
force_refresh = True force_refresh = True
elif have_data_entry(form_data, "convertmweb_" + cid):
txid = swap_client.ci(coin_id).convertMWEBBalance()
messages.append(f"Converted MWEB change to LTC in tx: {txid}")
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): elif have_data_entry(form_data, "newsparkaddr_" + cid):
@@ -525,6 +528,10 @@ def page_wallet(self, url_split, post_string):
// page_data["fee_estimate"]["sum_weight"] // page_data["fee_estimate"]["sum_weight"]
) )
if k == Coins.LTC and ci.useBackend() is False:
mweb_value: int = ci.getMWEBBalance()
if mweb_value > 0:
wallet_data["mweb_in_plain"] = ci.format_amount(mweb_value)
if show_utxo_groups: if show_utxo_groups:
utxo_groups = "" utxo_groups = ""
unspent_by_addr = ci.getUnspentsByAddr() unspent_by_addr = ci.getUnspentsByAddr()
+7
View File
@@ -0,0 +1,7 @@
# LTC Notes
## MWEB
Sending LTC -> MWEB generates MWEB change outputs in the plain LTC wallet that BSX can't use.
A temporary convenience function is provided to convert those MWEB outputs back to plain LTC.
@@ -15,14 +15,15 @@ export XMR_RPC_USER=xmr_user
export XMR_RPC_PWD=xmr_pwd export XMR_RPC_PWD=xmr_pwd
python tests/basicswap/extended/test_xmr_persistent.py python tests/basicswap/extended/test_xmr_persistent.py
# Copy coin releases to permanent storage for faster subsequent startups # Copy coin releases to permanent storage for faster subsequent startups
cp -r ${TEST_PATH}/bin/* ~/tmp/basicswap_bin/ cp -r ${TEST_PATH}/bin/* ~/tmp/basicswap_bin/
# Continue existing chains with # Continue existing chains with
export RESET_TEST=false export RESET_TEST=false
# Set coins started
export TEST_COINS_LIST="bitcoin,monero,litecoin"
""" """
import json import json
+70
View File
@@ -268,6 +268,62 @@ class TestLTC(BasicSwapTest):
if "mweb1" in addr: if "mweb1" in addr:
raise ValueError("getUnspentsByAddr should exclude mweb UTXOs.") raise ValueError("getUnspentsByAddr should exclude mweb UTXOs.")
# Test helper functions to convert MWEB change
mweb_change_value = ci0.getMWEBBalance()
assert mweb_change_value > 0
test_lock_utxo = None
for utxo in utxos:
utxo_address: str = utxo.get("address", "")
if any(
utxo_address.startswith(prefix) for prefix in ("ltcmweb1", "tmweb1")
):
continue
test_lock_utxo = {"txid": utxo["txid"], "vout": utxo["vout"]}
ci0.rpc_wallet(
"lockunspent",
[
False,
[
test_lock_utxo,
],
],
)
break
assert len(ci0.rpc_wallet("listlockunspent")) == 1
txid = ci0.convertMWEBBalance()
# Check utxos locked before conversion are still locked after
assert len(ci0.rpc_wallet("listlockunspent")) == 1
ci0.rpc_wallet(
"lockunspent",
[
True,
[
test_lock_utxo,
],
],
)
assert len(ci0.rpc_wallet("listlockunspent")) == 0
txj = ci0.rpc_wallet(
"gettransaction",
[
txid,
],
)
assert len(txj["details"]) == 2
fee_amt = -ci0.make_int(txj["fee"])
assert txj["details"][0]["category"] == "send"
assert ci0.make_int(txj["details"][0]["amount"]) - fee_amt == -mweb_change_value
assert txj["details"][1]["category"] == "receive"
assert ci0.make_int(txj["details"][1]["amount"]) + fee_amt == mweb_change_value
mweb_change_value = ci0.getMWEBBalance()
assert mweb_change_value == 0
# TODO # TODO
def test_22_mweb_balance(self): def test_22_mweb_balance(self):
@@ -362,6 +418,20 @@ class TestLTC(BasicSwapTest):
json_rv = read_json_api(TEST_HTTP_PORT + 0, "wallets/ltc", post_json) json_rv = read_json_api(TEST_HTTP_PORT + 0, "wallets/ltc", post_json)
assert json_rv["mweb_balance"] <= 20.0 assert json_rv["mweb_balance"] <= 20.0
# Test helper functions to convert MWEB change
json_rv = read_json_api(
TEST_HTTP_PORT + 0, "wallets/ltc/mwebbalance", post_json
)
assert float(json_rv) > 0
json_rv = read_json_api(
TEST_HTTP_PORT + 0, "wallets/ltc/convertmweb", post_json
)
assert len(json_rv) == 64
json_rv = read_json_api(
TEST_HTTP_PORT + 0, "wallets/ltc/mwebbalance", post_json
)
assert float(json_rv) == 0
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()