diff --git a/electrum/commands.py b/electrum/commands.py index 4639a5e5c..6bc172eb7 100644 --- a/electrum/commands.py +++ b/electrum/commands.py @@ -46,6 +46,7 @@ from . import util from .lnmsg import OnionWireSerializer from .logging import Logger from .onion_message import create_blinded_path, send_onion_message_to +from .submarine_swaps import NostrTransport from .util import ( bfh, json_decode, json_normalize, is_hash256_str, is_hex_str, to_bytes, parse_max_spend, to_decimal, UserFacingException, InvalidPassword @@ -1966,6 +1967,32 @@ class Commands(Logger): 'log': [x.formatted_tuple() for x in log] } + @command('wnl') + async def get_submarine_swap_providers(self, query_time=15, wallet: Abstract_Wallet = None): + """ + Queries nostr relays for available submarine swap providers. + + To configure one of the providers use: + setconfig swapserver_npub 'npub...' + + arg:int:query_time:Optional timeout how long the relays should be queried for provider announcements. Default: 15 sec + """ + sm = wallet.lnworker.swap_manager + async with sm.create_transport() as transport: + assert isinstance(transport, NostrTransport) + await asyncio.sleep(query_time) + offers = transport.get_recent_offers() + result = {} + for offer in offers: + result[offer.server_npub] = { + "percentage_fee": offer.pairs.percentage, + "max_forward_sat": offer.pairs.max_forward, + "max_reverse_sat": offer.pairs.max_reverse, + "min_amount_sat": offer.pairs.min_amount, + "provider_mining_fee": offer.pairs.mining_fee, + } + return result + @command('wnpl') async def normal_swap(self, onchain_amount, lightning_amount, password=None, wallet: Abstract_Wallet = None): """ @@ -1975,8 +2002,13 @@ class Commands(Logger): arg:decimal_or_dryrun:onchain_amount:Amount to be sent, in BTC. Set it to 'dryrun' to receive a value """ sm = wallet.lnworker.swap_manager + assert self.config.SWAPSERVER_NPUB or self.config.SWAPSERVER_URL, \ + "Configure swap provider first. See 'get_submarine_swap_providers'." async with sm.create_transport() as transport: - await sm.is_initialized.wait() + try: + await asyncio.wait_for(sm.is_initialized.wait(), timeout=15) + except asyncio.TimeoutError: + raise TimeoutError("Could not find configured swap provider. Setup another one. See 'get_submarine_swap_providers'") if lightning_amount == 'dryrun': onchain_amount_sat = satoshis(onchain_amount) lightning_amount_sat = sm.get_recv_amount(onchain_amount_sat, is_reverse=False) @@ -2013,8 +2045,13 @@ class Commands(Logger): arg:decimal_or_dryrun:provider_mining_fee:Mining fee required by the swap provider, in BTC. Set it to 'dryrun' to receive a value """ sm = wallet.lnworker.swap_manager + assert self.config.SWAPSERVER_NPUB or self.config.SWAPSERVER_URL, \ + "Configure swap provider first. See 'get_submarine_swap_providers'." async with sm.create_transport() as transport: - await sm.is_initialized.wait() + try: + await asyncio.wait_for(sm.is_initialized.wait(), timeout=15) + except asyncio.TimeoutError: + raise TimeoutError("Could not find configured swap provider. Setup another one. See 'get_submarine_swap_providers'") if onchain_amount == 'dryrun': lightning_amount_sat = satoshis(lightning_amount) onchain_amount_sat = sm.get_recv_amount(lightning_amount_sat, is_reverse=True) diff --git a/tests/regtest/regtest.sh b/tests/regtest/regtest.sh index 0c64ae0f6..2d61e2075 100755 --- a/tests/regtest/regtest.sh +++ b/tests/regtest/regtest.sh @@ -248,7 +248,8 @@ if [[ $1 == "swapserver_success" ]]; then echo "alice initiates swap" dryrun=$($alice reverse_swap 0.02 dryrun) onchain_amount=$(echo $dryrun| jq -r ".onchain_amount") - swap=$($alice reverse_swap 0.02 $onchain_amount) + swapserver_mining_fee=$(echo $dryrun| jq -r ".provider_mining_fee") + swap=$($alice reverse_swap 0.02 $onchain_amount --provider_mining_fee $swapserver_mining_fee) echo $swap | jq funding_txid=$(echo $swap| jq -r ".funding_txid") new_blocks 1 @@ -272,7 +273,8 @@ if [[ $1 == "swapserver_forceclose" ]]; then echo "alice initiates swap" dryrun=$($alice reverse_swap 0.02 dryrun) onchain_amount=$(echo $dryrun| jq -r ".onchain_amount") - swap=$($alice reverse_swap 0.02 $onchain_amount) + swapserver_mining_fee=$(echo $dryrun| jq -r ".provider_mining_fee") + swap=$($alice reverse_swap 0.02 $onchain_amount --provider_mining_fee $swapserver_mining_fee) echo $swap | jq funding_txid=$(echo $swap| jq -r ".funding_txid") ctx_id=$($bob close_channel --force $channel) @@ -307,7 +309,8 @@ if [[ $1 == "swapserver_refund" ]]; then echo "alice initiates swap" dryrun=$($alice reverse_swap 0.02 dryrun) onchain_amount=$(echo $dryrun| jq -r ".onchain_amount") - swap=$($alice reverse_swap 0.02 $onchain_amount) + swapserver_mining_fee=$(echo $dryrun| jq -r ".provider_mining_fee") + swap=$($alice reverse_swap 0.02 $onchain_amount --provider_mining_fee $swapserver_mining_fee) echo $swap | jq funding_txid=$(echo $swap| jq -r ".funding_txid") new_blocks 140 diff --git a/tests/test_commands.py b/tests/test_commands.py index 2f91b9371..2a93d099d 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -14,6 +14,7 @@ from electrum.lnworker import RecvMPPResolution from electrum.wallet import Abstract_Wallet from electrum.address_synchronizer import TX_HEIGHT_UNCONFIRMED from electrum.simple_config import SimpleConfig +from electrum.submarine_swaps import SwapOffer, SwapFees, NostrTransport from electrum.transaction import Transaction, TxOutput, tx_from_any from electrum.util import UserFacingException, NotEnoughFunds from electrum.crypto import sha256 @@ -640,3 +641,69 @@ class TestCommandsTestnet(ElectrumTestCase): "fiat_value": "-40.51", } ) + + @mock.patch.object(wallet.Abstract_Wallet, 'save_db') + async def test_get_submarine_swap_providers(self, *mock_args): + wallet = restore_wallet_from_text__for_unittest( + 'disagree rug lemon bean unaware square alone beach tennis exhibit fix mimic', + path='if_this_exists_mocking_failed_648151893', + config=self.config)['wallet'] + + cmds = Commands(config=self.config) + + offer1 = SwapOffer( + pairs=SwapFees( + percentage=0.5, + mining_fee=2000, + min_amount=10000, + max_forward=1000000, + max_reverse=500000 + ), + relays=["wss://relay1.example.com", "wss://relay2.example.com"], + timestamp=1640995200, + server_pubkey="a8cffad54f59e2c50a1d40ec0d57f1fc32df9cd2101fad8000215eb4a75b334d", + pow_bits=10 + ) + + offer2 = SwapOffer( + pairs=SwapFees( + percentage=1.0, + mining_fee=3000, + min_amount=20000, + max_forward=2000000, + max_reverse=1000000 + ), + relays=["ws://relay3.example.onion", "wss://relay4.example.com"], + timestamp=1640995300, + server_pubkey="7a483b6546be900481f6be2d2cc1b47c779ee89b4b66d1a066a8dc81c63ad1f0", + pow_bits=12 + ) + mock_offers = [offer1, offer2] + mock_transport = mock.Mock(NostrTransport) + mock_transport.get_recent_offers.return_value = mock_offers + + with mock.patch.object( + wallet.lnworker.swap_manager, + 'create_transport' + ) as mock_create_transport: + mock_create_transport.return_value.__aenter__.return_value = mock_transport + + result = await cmds.get_submarine_swap_providers(query_time=1, wallet=wallet) + + expected_result = { + offer1.server_npub: { + "percentage_fee": offer1.pairs.percentage, + "max_forward_sat": offer1.pairs.max_forward, + "max_reverse_sat": offer1.pairs.max_reverse, + "min_amount_sat": offer1.pairs.min_amount, + "provider_mining_fee": offer1.pairs.mining_fee, + }, + offer2.server_npub: { + "percentage_fee": offer2.pairs.percentage, + "max_forward_sat": offer2.pairs.max_forward, + "max_reverse_sat": offer2.pairs.max_reverse, + "min_amount_sat": offer2.pairs.min_amount, + "provider_mining_fee": offer2.pairs.mining_fee, + } + } + self.assertEqual(result, expected_result)