announce actual available liquidity as swap provider
This commit is contained in:
@@ -30,7 +30,7 @@ class QESwapServerNPubListModel(QAbstractListModel):
|
||||
_logger = get_logger(__name__)
|
||||
|
||||
# define listmodel rolemap
|
||||
_ROLE_NAMES= ('npub', 'timestamp', 'percentage_fee', 'mining_fee', 'min_amount', 'max_amount')
|
||||
_ROLE_NAMES= ('npub', 'timestamp', 'percentage_fee', 'mining_fee', 'min_amount', 'max_forward_amount', 'max_reverse_amount')
|
||||
_ROLE_KEYS = range(Qt.ItemDataRole.UserRole, Qt.ItemDataRole.UserRole + len(_ROLE_NAMES))
|
||||
_ROLE_MAP = dict(zip(_ROLE_KEYS, [bytearray(x.encode()) for x in _ROLE_NAMES]))
|
||||
|
||||
@@ -71,7 +71,8 @@ class QESwapServerNPubListModel(QAbstractListModel):
|
||||
'percentage_fee': x['percentage_fee'],
|
||||
'mining_fee': x['mining_fee'],
|
||||
'min_amount': x['min_amount'],
|
||||
'max_amount': x['max_amount'],
|
||||
'max_forward_amount': x['max_forward_amount'],
|
||||
'max_reverse_amount': x['max_reverse_amount'],
|
||||
'timestamp': age(x['timestamp']),
|
||||
} for x in items]
|
||||
self.endInsertRows()
|
||||
@@ -430,12 +431,12 @@ class QESwapHelper(AuthMixin, QObject, QtEventListener):
|
||||
except AttributeError: # happens if there are no utxos
|
||||
max_onchain_spend = 0
|
||||
reverse = int(min(lnworker.num_sats_can_send(),
|
||||
swap_manager.get_max_amount()))
|
||||
max_recv_amt_ln = min(swap_manager.get_max_amount(), int(lnworker.num_sats_can_receive()))
|
||||
swap_manager.get_provider_max_forward_amount()))
|
||||
max_recv_amt_ln = min(swap_manager.get_provider_max_reverse_amount(), int(lnworker.num_sats_can_receive()))
|
||||
max_recv_amt_oc = swap_manager.get_send_amount(max_recv_amt_ln, is_reverse=False) or 0
|
||||
forward = int(min(max_recv_amt_oc,
|
||||
# maximally supported swap amount by provider
|
||||
swap_manager.get_max_amount(),
|
||||
swap_manager.get_provider_max_reverse_amount(),
|
||||
max_onchain_spend))
|
||||
# we expect range to adjust the value of the swap slider to be in the
|
||||
# correct range, i.e., to correct an overflow when reducing the limits
|
||||
@@ -597,9 +598,9 @@ class QESwapHelper(AuthMixin, QObject, QtEventListener):
|
||||
coins = self._wallet.wallet.get_spendable_coins()
|
||||
if onchain_amount == '!':
|
||||
max_amount = sum(c.value_sats() for c in coins)
|
||||
max_swap_amount = self._wallet.wallet.lnworker.swap_manager.max_amount_forward_swap()
|
||||
max_swap_amount = self._wallet.wallet.lnworker.swap_manager.client_max_amount_forward_swap()
|
||||
if max_swap_amount is None:
|
||||
raise InvalidSwapParameters("swap_manager.max_amount_forward_swap() is None")
|
||||
raise InvalidSwapParameters("swap_manager.client_max_amount_forward_swap() is None")
|
||||
if max_amount > max_swap_amount:
|
||||
onchain_amount = max_swap_amount
|
||||
outputs = [PartialTxOutput.from_address_and_value(DummyAddress.SWAP, onchain_amount)]
|
||||
|
||||
@@ -177,7 +177,7 @@ class SwapDialog(WindowModalDialog, QtEventListener):
|
||||
self.max_button.setChecked(False)
|
||||
|
||||
def _spend_max_reverse_swap(self) -> None:
|
||||
amount = min(self.lnworker.num_sats_can_send(), self.swap_manager.get_max_amount())
|
||||
amount = min(self.lnworker.num_sats_can_send(), self.swap_manager.get_provider_max_forward_amount())
|
||||
amount = int(amount) # round down msats
|
||||
self.send_amount_e.setAmount(amount)
|
||||
|
||||
@@ -295,9 +295,9 @@ class SwapDialog(WindowModalDialog, QtEventListener):
|
||||
coins = self.window.get_coins()
|
||||
if onchain_amount == '!':
|
||||
max_amount = sum(c.value_sats() for c in coins)
|
||||
max_swap_amount = self.swap_manager.max_amount_forward_swap()
|
||||
max_swap_amount = self.swap_manager.client_max_amount_forward_swap()
|
||||
if max_swap_amount is None:
|
||||
raise InvalidSwapParameters("swap_manager.max_amount_forward_swap() is None")
|
||||
raise InvalidSwapParameters("swap_manager.client_max_amount_forward_swap() is None")
|
||||
if max_amount > max_swap_amount:
|
||||
onchain_amount = max_swap_amount
|
||||
outputs = [PartialTxOutput.from_address_and_value(DummyAddress.SWAP, onchain_amount)]
|
||||
|
||||
@@ -242,7 +242,7 @@ class UTXOList(MyTreeView):
|
||||
return False
|
||||
value = sum(x.value_sats() for x in coins)
|
||||
min_amount = self.wallet.lnworker.swap_manager.get_min_amount()
|
||||
max_amount = self.wallet.lnworker.swap_manager.max_amount_forward_swap()
|
||||
max_amount = self.wallet.lnworker.swap_manager.client_max_amount_forward_swap()
|
||||
if min_amount is None or max_amount is None:
|
||||
# we need to fetch data from swap server
|
||||
return True
|
||||
|
||||
@@ -66,7 +66,9 @@ class HttpSwapServer(Logger, EventListener):
|
||||
"BTC/BTC": {
|
||||
"rate": 1,
|
||||
"limits": {
|
||||
"maximal": sm._max_amount,
|
||||
"maximal": min(sm._max_forward, sm._max_reverse), # legacy
|
||||
"max_forward_amount": sm._max_forward, # new version, uses 2 separate limits
|
||||
"max_reverse_amount": sm._max_reverse,
|
||||
"minimal": sm._min_amount,
|
||||
},
|
||||
"fees": {
|
||||
|
||||
@@ -26,8 +26,8 @@ from .bitcoin import (script_to_p2wsh, opcodes,
|
||||
from .transaction import PartialTxInput, PartialTxOutput, PartialTransaction, Transaction, TxInput, TxOutpoint
|
||||
from .transaction import script_GetOp, match_script_against_template, OPPushDataGeneric, OPPushDataPubkey
|
||||
from .util import (log_exceptions, ignore_exceptions, BelowDustLimit, OldTaskGroup, age, ca_path,
|
||||
gen_nostr_ann_pow, get_nostr_ann_pow_amount, make_aiohttp_proxy_connector, get_running_loop,
|
||||
get_asyncio_loop)
|
||||
gen_nostr_ann_pow, get_nostr_ann_pow_amount, make_aiohttp_proxy_connector,
|
||||
get_running_loop, get_asyncio_loop, wait_for2)
|
||||
from .lnutil import REDEEM_AFTER_DOUBLE_SPENT_DELAY
|
||||
from .bitcoin import dust_threshold, DummyAddress
|
||||
from .logging import Logger
|
||||
@@ -134,7 +134,8 @@ class SwapFees:
|
||||
percentage = attr.ib(type=int)
|
||||
mining_fee = attr.ib(type=int)
|
||||
min_amount = attr.ib(type=int)
|
||||
max_amount = attr.ib(type=int)
|
||||
max_forward = attr.ib(type=int)
|
||||
max_reverse = attr.ib(type=int)
|
||||
|
||||
@stored_in('submarine_swaps')
|
||||
@attr.s
|
||||
@@ -175,7 +176,8 @@ class SwapManager(Logger):
|
||||
self.mining_fee = None
|
||||
self.percentage = None
|
||||
self._min_amount = None
|
||||
self._max_amount = None
|
||||
self._max_forward = None
|
||||
self._max_reverse = None
|
||||
|
||||
self.wallet = wallet
|
||||
self.config = wallet.config
|
||||
@@ -202,6 +204,7 @@ class SwapManager(Logger):
|
||||
self.is_server = False # overriden by swapserver plugin if enabled
|
||||
self.is_initialized = asyncio.Event()
|
||||
self.pairs_updated = asyncio.Event()
|
||||
self._liquidity_changed = asyncio.Event()
|
||||
|
||||
def start_network(self, network: 'Network'):
|
||||
assert network
|
||||
@@ -220,13 +223,22 @@ class SwapManager(Logger):
|
||||
async def run_nostr_server(self):
|
||||
await self.set_nostr_proof_of_work()
|
||||
with NostrTransport(self.config, self, self.lnworker.nostr_keypair) as transport:
|
||||
# wait a bit so we don't publish 0 liquidity on startup if channels are not yet reestablished
|
||||
await asyncio.sleep(10)
|
||||
await transport.is_connected.wait()
|
||||
self.logger.info(f'nostr is connected')
|
||||
# will publish a new announcement if liquidity changed or every OFFER_UPDATE_INTERVAL_SEC
|
||||
while True:
|
||||
# todo: publish everytime fees have changed
|
||||
self.server_update_pairs()
|
||||
await transport.publish_offer(self)
|
||||
await asyncio.sleep(transport.OFFER_UPDATE_INTERVAL_SEC)
|
||||
try:
|
||||
await wait_for2(
|
||||
self._liquidity_changed.wait(),
|
||||
timeout=transport.OFFER_UPDATE_INTERVAL_SEC
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
continue
|
||||
|
||||
@log_exceptions
|
||||
async def main_loop(self):
|
||||
@@ -427,6 +439,7 @@ class SwapManager(Logger):
|
||||
except BelowDustLimit:
|
||||
self.logger.info('utxo value below dust threshold')
|
||||
return
|
||||
self.server_maybe_trigger_liquidity_update()
|
||||
|
||||
def get_swap_tx_fee(self):
|
||||
return self._get_tx_fee(self.config.FEE_POLICY)
|
||||
@@ -470,6 +483,8 @@ class SwapManager(Logger):
|
||||
our_privkey = os.urandom(32)
|
||||
our_pubkey = ECPrivkey(our_privkey).get_public_key_bytes(compressed=True)
|
||||
onchain_amount_sat = self._get_recv_amount(lightning_amount_sat, is_reverse=True) # what the client is going to receive
|
||||
if not onchain_amount_sat:
|
||||
raise Exception("no onchain amount")
|
||||
redeem_script = construct_script(
|
||||
WITNESS_TEMPLATE_REVERSE_SWAP,
|
||||
values={1:32, 5:ripemd(payment_hash), 7:their_pubkey, 10:locktime, 13:our_pubkey}
|
||||
@@ -563,6 +578,8 @@ class SwapManager(Logger):
|
||||
privkey = os.urandom(32)
|
||||
our_pubkey = ECPrivkey(privkey).get_public_key_bytes(compressed=True)
|
||||
onchain_amount_sat = self._get_send_amount(lightning_amount_sat, is_reverse=False)
|
||||
if not onchain_amount_sat:
|
||||
raise Exception("no onchain amount")
|
||||
preimage = os.urandom(32)
|
||||
payment_hash = sha256(preimage)
|
||||
redeem_script = construct_script(
|
||||
@@ -907,17 +924,35 @@ class SwapManager(Logger):
|
||||
|
||||
def server_update_pairs(self) -> None:
|
||||
""" for server """
|
||||
self.percentage = float(self.config.SWAPSERVER_FEE_MILLIONTHS) / 10000
|
||||
self.percentage = float(self.config.SWAPSERVER_FEE_MILLIONTHS) / 10000 # type: ignore
|
||||
self._min_amount = 20000
|
||||
self._max_amount = 10000000
|
||||
anchor_reserve = self.config.LN_UTXO_RESERVE \
|
||||
if (any(chan.has_anchors() and not chan.is_redeemed()
|
||||
for chan in self.lnworker.channels.values())) else 0
|
||||
oc_balance = max(sum([coin.value_sats() for coin in self.wallet.get_spendable_coins()])
|
||||
- anchor_reserve, 0)
|
||||
max_forward: int = min(int(self.lnworker.num_sats_can_receive()), oc_balance, 10000000)
|
||||
max_reverse: int = min(int(self.lnworker.num_sats_can_send()), 10000000)
|
||||
self._max_forward: int = self._keep_leading_digits(max_forward, 2)
|
||||
self._max_reverse: int = self._keep_leading_digits(max_reverse, 2)
|
||||
self.mining_fee = self.get_fee_for_txbatcher()
|
||||
|
||||
@staticmethod
|
||||
def _keep_leading_digits(num: int, digits: int) -> int:
|
||||
"""Reduces precision of num to `digits` leading digits."""
|
||||
if num <= 0:
|
||||
return 0
|
||||
num_str = str(num)
|
||||
zeroed_num_str = f"{num_str[:digits]}{(len(num_str[digits:])) * '0'}"
|
||||
return int(zeroed_num_str)
|
||||
|
||||
def update_pairs(self, pairs):
|
||||
self.logger.info(f'updating fees {pairs}')
|
||||
self.mining_fee = pairs.mining_fee
|
||||
self.percentage = pairs.percentage
|
||||
self._min_amount = pairs.min_amount
|
||||
self._max_amount = pairs.max_amount
|
||||
self._max_forward = pairs.max_forward
|
||||
self._max_reverse = pairs.max_reverse
|
||||
self.trigger_pairs_updated_threadsafe()
|
||||
|
||||
def trigger_pairs_updated_threadsafe(self):
|
||||
@@ -928,16 +963,41 @@ class SwapManager(Logger):
|
||||
loop = get_asyncio_loop()
|
||||
loop.call_soon_threadsafe(trigger)
|
||||
|
||||
def get_max_amount(self) -> int:
|
||||
"""in satoshis"""
|
||||
return self._max_amount
|
||||
def server_maybe_trigger_liquidity_update(self) -> None:
|
||||
"""
|
||||
To be called when the available liquidity changes so the new liquidity is announced.
|
||||
(ln in/out, onchain in/out)
|
||||
"""
|
||||
if not self.is_server:
|
||||
return
|
||||
assert get_running_loop() == get_asyncio_loop(), "Events must be set in the asyncio thread"
|
||||
previous_max_forward = self._max_forward
|
||||
previous_max_reverse = self._max_reverse
|
||||
self.server_update_pairs()
|
||||
# if liquidity really changed the event is triggered so a new provider announcement is published
|
||||
if self._max_forward != previous_max_forward or self._max_reverse != previous_max_reverse:
|
||||
self.logger.debug(f"liquidity changed, updating announcement")
|
||||
self._liquidity_changed.set()
|
||||
self._liquidity_changed.clear()
|
||||
|
||||
def get_provider_max_forward_amount(self) -> int:
|
||||
"""in sat"""
|
||||
return self._max_forward
|
||||
|
||||
def get_provider_max_reverse_amount(self) -> int:
|
||||
"""in sat"""
|
||||
return self._max_reverse
|
||||
|
||||
def get_min_amount(self) -> int:
|
||||
"""in satoshis"""
|
||||
return self._min_amount
|
||||
|
||||
def check_invoice_amount(self, x) -> bool:
|
||||
return self.get_min_amount() <= x <= self.get_max_amount()
|
||||
def check_invoice_amount(self, x, is_reverse: bool) -> bool:
|
||||
if is_reverse:
|
||||
max_amount = self.get_provider_max_forward_amount()
|
||||
else:
|
||||
max_amount = self.get_provider_max_reverse_amount()
|
||||
return self.get_min_amount() <= x <= max_amount
|
||||
|
||||
def _get_recv_amount(self, send_amount: Optional[int], *, is_reverse: bool) -> Optional[int]:
|
||||
"""For a given swap direction and amount we send, returns how much we will receive.
|
||||
@@ -946,12 +1006,12 @@ class SwapManager(Logger):
|
||||
In the reverse direction, the result matches what the swap server returns as response["onchainAmount"].
|
||||
"""
|
||||
if send_amount is None:
|
||||
return
|
||||
return None
|
||||
x = Decimal(send_amount)
|
||||
percentage = Decimal(self.percentage)
|
||||
if is_reverse:
|
||||
if not self.check_invoice_amount(x):
|
||||
return
|
||||
if not self.check_invoice_amount(x, is_reverse):
|
||||
return None
|
||||
# see/ref:
|
||||
# https://github.com/BoltzExchange/boltz-backend/blob/e7e2d30f42a5bea3665b164feb85f84c64d86658/lib/service/Service.ts#L948
|
||||
percentage_fee = math.ceil(percentage * x / 100)
|
||||
@@ -959,13 +1019,13 @@ class SwapManager(Logger):
|
||||
x -= percentage_fee + base_fee
|
||||
x = math.floor(x)
|
||||
if x < dust_threshold():
|
||||
return
|
||||
return None
|
||||
else:
|
||||
x -= self.mining_fee
|
||||
percentage_fee = math.ceil(x * percentage / (100 + percentage))
|
||||
x -= percentage_fee
|
||||
if not self.check_invoice_amount(x):
|
||||
return
|
||||
if not self.check_invoice_amount(x, is_reverse):
|
||||
return None
|
||||
x = int(x)
|
||||
return x
|
||||
|
||||
@@ -976,7 +1036,7 @@ class SwapManager(Logger):
|
||||
In the forward direction, the result matches what the swap server returns as response["expectedAmount"].
|
||||
"""
|
||||
if not recv_amount:
|
||||
return
|
||||
return None
|
||||
x = Decimal(recv_amount)
|
||||
percentage = Decimal(self.percentage)
|
||||
if is_reverse:
|
||||
@@ -986,11 +1046,11 @@ class SwapManager(Logger):
|
||||
base_fee = self.mining_fee
|
||||
x += base_fee
|
||||
x = math.ceil(x / ((100 - percentage) / 100))
|
||||
if not self.check_invoice_amount(x):
|
||||
return
|
||||
if not self.check_invoice_amount(x, is_reverse):
|
||||
return None
|
||||
else:
|
||||
if not self.check_invoice_amount(x):
|
||||
return
|
||||
if not self.check_invoice_amount(x, is_reverse):
|
||||
return None
|
||||
# see/ref:
|
||||
# https://github.com/BoltzExchange/boltz-backend/blob/e7e2d30f42a5bea3665b164feb85f84c64d86658/lib/service/Service.ts#L708
|
||||
# https://github.com/BoltzExchange/boltz-backend/blob/e7e2d30f42a5bea3665b164feb85f84c64d86658/lib/rates/FeeProvider.ts#L90
|
||||
@@ -1036,7 +1096,7 @@ class SwapManager(Logger):
|
||||
swaps.append(swap)
|
||||
return swaps
|
||||
|
||||
def get_swaps_by_claim_tx(self, tx: Transaction) -> Iterable[SwapData]:
|
||||
def get_swaps_by_claim_tx(self, tx: Transaction) -> Iterable[Tuple[int, SwapData]]:
|
||||
swaps = []
|
||||
for i, txin in enumerate(tx.inputs()):
|
||||
if swap := self.get_swap_by_claim_txin(txin):
|
||||
@@ -1085,9 +1145,9 @@ class SwapManager(Logger):
|
||||
txin.make_witness = make_witness
|
||||
return txin, locktime
|
||||
|
||||
def max_amount_forward_swap(self) -> Optional[int]:
|
||||
def client_max_amount_forward_swap(self) -> Optional[int]:
|
||||
""" returns None if we cannot swap """
|
||||
max_swap_amt_ln = self.get_max_amount()
|
||||
max_swap_amt_ln = self.get_provider_max_reverse_amount()
|
||||
if max_swap_amt_ln is None:
|
||||
return None
|
||||
max_recv_amt_ln = int(self.lnworker.num_sats_can_receive())
|
||||
@@ -1261,7 +1321,8 @@ class HttpTransport(SwapServerTransport):
|
||||
percentage=fees['percentage'],
|
||||
mining_fee=fees['minerFees']['baseAsset']['mining_fee'],
|
||||
min_amount=limits['minimal'],
|
||||
max_amount=limits['maximal'],
|
||||
max_forward=limits['max_forward_amount'],
|
||||
max_reverse=limits['max_reverse_amount'],
|
||||
)
|
||||
self.sm.update_pairs(pairs)
|
||||
|
||||
@@ -1274,7 +1335,7 @@ class NostrTransport(SwapServerTransport):
|
||||
|
||||
EPHEMERAL_REQUEST = 25582
|
||||
USER_STATUS_NIP38 = 30315
|
||||
NOSTR_EVENT_VERSION = 4
|
||||
NOSTR_EVENT_VERSION = 5
|
||||
OFFER_UPDATE_INTERVAL_SEC = 60 * 10
|
||||
|
||||
def __init__(self, config, sm, keypair):
|
||||
@@ -1376,18 +1437,23 @@ class NostrTransport(SwapServerTransport):
|
||||
percentage=offer['percentage_fee'],
|
||||
mining_fee=offer['mining_fee'],
|
||||
min_amount=offer['min_amount'],
|
||||
max_amount=offer['max_amount'],
|
||||
max_forward=offer['max_forward_amount'],
|
||||
max_reverse=offer['max_reverse_amount'],
|
||||
)
|
||||
|
||||
@ignore_exceptions
|
||||
@log_exceptions
|
||||
async def publish_offer(self, sm):
|
||||
async def publish_offer(self, sm) -> None:
|
||||
assert self.sm.is_server
|
||||
if sm._max_forward < sm._min_amount and sm._max_reverse < sm._min_amount:
|
||||
self.logger.warning(f"not publishing swap offer, no liquidity available: {sm._max_forward=}, {sm._max_reverse=}")
|
||||
return
|
||||
offer = {
|
||||
'percentage_fee': sm.percentage,
|
||||
'mining_fee': sm.mining_fee,
|
||||
'min_amount': sm._min_amount,
|
||||
'max_amount': sm._max_amount,
|
||||
'max_forward_amount': sm._max_forward,
|
||||
'max_reverse_amount': sm._max_reverse,
|
||||
'relays': sm.config.NOSTR_RELAYS,
|
||||
'pow_nonce': hex(sm.config.SWAPSERVER_ANN_POW_NONCE),
|
||||
}
|
||||
@@ -1544,6 +1610,7 @@ class NostrTransport(SwapServerTransport):
|
||||
r['reply_to'] = event_id
|
||||
self.logger.debug(f'sending response id={event_id}')
|
||||
await self.send_direct_message(event_pubkey, json.dumps(r))
|
||||
self.sm.server_maybe_trigger_liquidity_update()
|
||||
|
||||
def _store_last_swapserver_relays(self, relays: Sequence[str]):
|
||||
self._last_swapserver_relays = relays
|
||||
|
||||
Reference in New Issue
Block a user