swaps: revise send/recv amount calculation
- document SwapManager._get_recv_amount and SwapManager._get_send_amount
- change calculations so that they match the boltz-backend
- note that in the reverse swap case, the server does not care about the on-chain claim tx the client
needs to pay for. This introduced some implicit hacks and inconsistencies in the code in the past,
it is still a bit ugly but at least this is now explicit.
- SwapManager._get_recv_amount and SwapManager._get_send_amount are now proper inverses of each other
-----
Here are some code snippets to play around with in Qt console.
For the forward swap case:
```
from electrum import ecc; lnworker = wallet.lnworker; sm = lnworker.swap_manager
invoice = network.run_from_another_thread(lnworker.create_invoice(amount_msat=3000000*1000, message="swap", expiry=86400))[1]; request_data = {"type": "submarine", "pairId": "BTC/BTC", "orderSide": "sell", "invoice": invoice, "refundPublicKey": ecc.GENERATOR.get_public_key_bytes().hex()}
network.send_http_on_proxy('post', sm.api_url + '/createswap', json=request_data, timeout=30)
sm.get_send_amount(3000000, is_reverse=False)
sm.get_recv_amount(3026730, is_reverse=False)
```
For the reverse swap case:
```
from electrum import ecc; import os; lnworker = wallet.lnworker; sm = lnworker.swap_manager
request_data = {"type": "reversesubmarine", "pairId": "BTC/BTC", "orderSide": "buy", "invoiceAmount": 3000000, "preimageHash": os.urandom(32).hex(), "claimPublicKey": ecc.GENERATOR.get_public_key_bytes().hex()}
network.send_http_on_proxy('post', sm.api_url + '/createswap', json=request_data, timeout=30)
sm.get_recv_amount(3000000, is_reverse=True)
sm.get_send_amount(2974443, is_reverse=True)
```
This commit is contained in:
@@ -1189,7 +1189,8 @@ class Commands:
|
||||
success = None
|
||||
else:
|
||||
lightning_amount_sat = satoshis(lightning_amount)
|
||||
onchain_amount_sat = satoshis(onchain_amount)
|
||||
claim_fee = sm.get_claim_fee()
|
||||
onchain_amount_sat = satoshis(onchain_amount + claim_fee)
|
||||
success = await wallet.lnworker.swap_manager.reverse_swap(
|
||||
lightning_amount_sat=lightning_amount_sat,
|
||||
expected_onchain_amount_sat=onchain_amount_sat,
|
||||
|
||||
@@ -145,7 +145,7 @@ class SwapDialog(WindowModalDialog):
|
||||
return
|
||||
self.send_amount_e.setStyleSheet(ColorScheme.DEFAULT.as_stylesheet())
|
||||
send_amount = self.send_amount_e.get_amount()
|
||||
recv_amount = self.swap_manager.get_recv_amount(send_amount, self.is_reverse)
|
||||
recv_amount = self.swap_manager.get_recv_amount(send_amount, is_reverse=self.is_reverse)
|
||||
if self.is_reverse and send_amount and send_amount > self.lnworker.num_sats_can_send():
|
||||
# cannot send this much on lightning
|
||||
recv_amount = None
|
||||
@@ -166,7 +166,7 @@ class SwapDialog(WindowModalDialog):
|
||||
return
|
||||
self.recv_amount_e.setStyleSheet(ColorScheme.DEFAULT.as_stylesheet())
|
||||
recv_amount = self.recv_amount_e.get_amount()
|
||||
send_amount = self.swap_manager.get_send_amount(recv_amount, self.is_reverse)
|
||||
send_amount = self.swap_manager.get_send_amount(recv_amount, is_reverse=self.is_reverse)
|
||||
if self.is_reverse and send_amount and send_amount > self.lnworker.num_sats_can_send():
|
||||
send_amount = None
|
||||
self.send_amount_e.follows = True
|
||||
|
||||
@@ -2,6 +2,8 @@ import asyncio
|
||||
import json
|
||||
import os
|
||||
from typing import TYPE_CHECKING, Optional, Dict, Union
|
||||
from decimal import Decimal
|
||||
import math
|
||||
|
||||
import attr
|
||||
|
||||
@@ -182,6 +184,8 @@ class SwapManager(Logger):
|
||||
self.lnwatcher.remove_callback(swap.lockup_address)
|
||||
swap.is_redeemed = True
|
||||
continue
|
||||
# FIXME the mining fee should depend on swap.is_reverse.
|
||||
# the txs are not the same size...
|
||||
amount_sat = txin.value_sats() - self.get_claim_fee()
|
||||
if amount_sat < dust_threshold():
|
||||
self.logger.info('utxo value below dust threshold')
|
||||
@@ -339,6 +343,8 @@ class SwapManager(Logger):
|
||||
- Server creates on-chain output locked to RHASH.
|
||||
- User spends on-chain output, revealing preimage.
|
||||
- Server fulfills HTLC using preimage.
|
||||
|
||||
Note: expected_onchain_amount_sat is BEFORE deducting the on-chain claim tx fee.
|
||||
"""
|
||||
assert self.network
|
||||
assert self.lnwatcher
|
||||
@@ -449,39 +455,88 @@ class SwapManager(Logger):
|
||||
def check_invoice_amount(self, x):
|
||||
return x >= self.min_amount and x <= self._max_amount
|
||||
|
||||
def get_recv_amount(self, send_amount: Optional[int], is_reverse: bool) -> Optional[int]:
|
||||
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.
|
||||
|
||||
Note: in the reverse direction, the mining fee for the on-chain claim tx is NOT accounted for.
|
||||
In the reverse direction, the result matches what the swap server returns as response["onchainAmount"].
|
||||
"""
|
||||
if send_amount is None:
|
||||
return
|
||||
x = send_amount
|
||||
x = Decimal(send_amount)
|
||||
percentage = Decimal(self.percentage)
|
||||
if is_reverse:
|
||||
if not self.check_invoice_amount(x):
|
||||
return
|
||||
x = int(x * (100 - self.percentage) / 100)
|
||||
x -= self.lockup_fee
|
||||
x -= self.get_claim_fee()
|
||||
# see/ref:
|
||||
# https://github.com/BoltzExchange/boltz-backend/blob/e7e2d30f42a5bea3665b164feb85f84c64d86658/lib/service/Service.ts#L948
|
||||
percentage_fee = math.ceil(percentage * x / 100)
|
||||
base_fee = self.lockup_fee
|
||||
x -= percentage_fee + base_fee
|
||||
x = math.floor(x)
|
||||
if x < dust_threshold():
|
||||
return
|
||||
else:
|
||||
x -= self.normal_fee
|
||||
x = int(x / ((100 + self.percentage) / 100))
|
||||
percentage_fee = math.ceil(x * percentage / (100 + percentage))
|
||||
x -= percentage_fee
|
||||
if not self.check_invoice_amount(x):
|
||||
return
|
||||
x = int(x)
|
||||
return x
|
||||
|
||||
def get_send_amount(self, recv_amount: Optional[int], is_reverse: bool) -> Optional[int]:
|
||||
def _get_send_amount(self, recv_amount: Optional[int], *, is_reverse: bool) -> Optional[int]:
|
||||
"""For a given swap direction and amount we want to receive, returns how much we will need to send.
|
||||
|
||||
Note: in the reverse direction, the mining fee for the on-chain claim tx is NOT accounted for.
|
||||
In the forward direction, the result matches what the swap server returns as response["expectedAmount"].
|
||||
"""
|
||||
if not recv_amount:
|
||||
return
|
||||
x = recv_amount
|
||||
x = Decimal(recv_amount)
|
||||
percentage = Decimal(self.percentage)
|
||||
if is_reverse:
|
||||
x += self.lockup_fee
|
||||
x += self.get_claim_fee()
|
||||
x = int(x * 100 / (100 - self.percentage)) + 1
|
||||
# see/ref:
|
||||
# https://github.com/BoltzExchange/boltz-backend/blob/e7e2d30f42a5bea3665b164feb85f84c64d86658/lib/service/Service.ts#L928
|
||||
# https://github.com/BoltzExchange/boltz-backend/blob/e7e2d30f42a5bea3665b164feb85f84c64d86658/lib/service/Service.ts#L958
|
||||
base_fee = self.lockup_fee
|
||||
x += base_fee
|
||||
x = math.ceil(x / ((100 - percentage) / 100))
|
||||
if not self.check_invoice_amount(x):
|
||||
return
|
||||
else:
|
||||
if not self.check_invoice_amount(x):
|
||||
return
|
||||
x = int(x * 100 / (100 + self.percentage)) + 1
|
||||
x += self.normal_fee
|
||||
# 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
|
||||
percentage_fee = math.ceil(percentage * x / 100)
|
||||
x += percentage_fee + self.normal_fee
|
||||
x = int(x)
|
||||
return x
|
||||
|
||||
def get_recv_amount(self, send_amount: Optional[int], *, is_reverse: bool) -> Optional[int]:
|
||||
recv_amount = self._get_recv_amount(send_amount, is_reverse=is_reverse)
|
||||
# sanity check calculation can be inverted
|
||||
if recv_amount is not None:
|
||||
inverted_recv_amount = self._get_send_amount(recv_amount, is_reverse=is_reverse)
|
||||
if send_amount != inverted_recv_amount:
|
||||
raise Exception(f"calc-invert-sanity-check failed. is_reverse={is_reverse}. "
|
||||
f"send_amount={send_amount} -> recv_amount={recv_amount} -> inverted_recv_amount={inverted_recv_amount}")
|
||||
# account for on-chain claim tx fee
|
||||
if is_reverse and recv_amount is not None:
|
||||
recv_amount -= self.get_claim_fee()
|
||||
return recv_amount
|
||||
|
||||
def get_send_amount(self, recv_amount: Optional[int], *, is_reverse: bool) -> Optional[int]:
|
||||
send_amount = self._get_send_amount(recv_amount, is_reverse=is_reverse)
|
||||
# sanity check calculation can be inverted
|
||||
if send_amount is not None:
|
||||
inverted_send_amount = self._get_recv_amount(send_amount, is_reverse=is_reverse)
|
||||
if recv_amount != inverted_send_amount:
|
||||
raise Exception(f"calc-invert-sanity-check failed. is_reverse={is_reverse}. "
|
||||
f"recv_amount={recv_amount} -> send_amount={send_amount} -> inverted_send_amount={inverted_send_amount}")
|
||||
# account for on-chain claim tx fee
|
||||
if is_reverse and send_amount is not None:
|
||||
send_amount += self.get_claim_fee()
|
||||
return send_amount
|
||||
|
||||
Reference in New Issue
Block a user