1
0

simplify submarine swap onchain fee model to single base fee

This commit is contained in:
f321x
2025-02-19 17:39:56 +01:00
parent 0bbdad3efa
commit 6f97b7b5f9
7 changed files with 43 additions and 59 deletions

View File

@@ -1414,7 +1414,7 @@ class Commands:
funding_txid = None
else:
lightning_amount_sat = satoshis(lightning_amount)
claim_fee = sm.get_claim_fee()
claim_fee = sm.get_swap_tx_fee()
onchain_amount_sat = satoshis(onchain_amount) + claim_fee
funding_txid = await wallet.lnworker.swap_manager.reverse_swap(
transport,

View File

@@ -29,8 +29,7 @@ class QESwapServerNPubListModel(QAbstractListModel):
_logger = get_logger(__name__)
# define listmodel rolemap
_ROLE_NAMES= ('npub', 'timestamp', 'percentage_fee', 'normal_mining_fee', 'reverse_mining_fee', 'claim_mining_fee',
'min_amount', 'max_amount')
_ROLE_NAMES= ('npub', 'timestamp', 'percentage_fee', 'mining_fee', 'min_amount', 'max_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]))
@@ -69,9 +68,7 @@ class QESwapServerNPubListModel(QAbstractListModel):
self._services = [{
'npub': x['pubkey'],
'percentage_fee': x['percentage_fee'],
'normal_mining_fee': x['normal_mining_fee'],
'reverse_mining_fee': x['reverse_mining_fee'],
'claim_mining_fee': x['claim_mining_fee'],
'mining_fee': x['mining_fee'],
'min_amount': x['min_amount'],
'max_amount': x['max_amount'],
'timestamp': age(x['timestamp']),
@@ -378,7 +375,6 @@ class QESwapHelper(AuthMixin, QObject, QtEventListener):
self._logger.error(str(e))
except RuntimeError:
pass
if isinstance(transport, NostrTransport):
if not swap_manager.is_initialized.is_set():
if not transport.is_connected.is_set():
@@ -502,7 +498,7 @@ class QESwapHelper(AuthMixin, QObject, QtEventListener):
self.toreceive = QEAmount(amount_sat=self._receive_amount)
# fee breakdown
self.serverfeeperc = f'{swap_manager.percentage:0.1f}%'
server_miningfee = swap_manager.lockup_fee if self.isReverse else swap_manager.normal_fee
server_miningfee = swap_manager.mining_fee
self.serverMiningfee = QEAmount(amount_sat=server_miningfee)
if self.isReverse:
self.check_valid(self._send_amount, self._receive_amount)
@@ -626,7 +622,7 @@ class QESwapHelper(AuthMixin, QObject, QtEventListener):
return await swap_manager.reverse_swap(
transport,
lightning_amount_sat=lightning_amount,
expected_onchain_amount_sat=onchain_amount + swap_manager.get_claim_fee(),
expected_onchain_amount_sat=onchain_amount + swap_manager.get_swap_tx_fee(),
)
try:
fut = asyncio.run_coroutine_threadsafe(coro(), loop)

View File

@@ -1247,7 +1247,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
def descr(x):
last_seen = util.age(x['timestamp'])
return (f"pubkey={x['pubkey'][0:10]}, "
f"fee={x['percentage_fee']}% + {x['reverse_mining_fee']} sats, "
f"fee={x['percentage_fee']}% + {x['mining_fee']} sats, "
f"last_seen: {last_seen}")
server_keys = [(x['pubkey'], descr(x)) for x in recent_offers]
msg = '\n'.join([

View File

@@ -239,7 +239,7 @@ class SwapDialog(WindowModalDialog, QtEventListener):
self.recv_label.setIcon(recv_icon)
self.description_label.setText(self.get_description())
self.description_label.repaint() # macOS hack for #6269
server_mining_fee = sm.lockup_fee if self.is_reverse else sm.normal_fee
server_mining_fee = sm.mining_fee
server_fee_str = '%.2f'%sm.percentage + '% + ' + self.window.format_amount(server_mining_fee) + ' ' + self.window.base_unit()
self.server_fee_label.setText(server_fee_str)
self.server_fee_label.repaint() # macOS hack for #6269
@@ -249,7 +249,7 @@ class SwapDialog(WindowModalDialog, QtEventListener):
"""Updates self.fee_label. No other side-effects."""
if self.is_reverse:
sm = self.swap_manager
fee = sm.get_claim_fee()
fee = sm.get_swap_tx_fee()
else:
fee = tx.get_fee() if tx else None
fee_text = self.window.format_amount(fee) + ' ' + self.window.base_unit() if fee else ''
@@ -269,7 +269,7 @@ class SwapDialog(WindowModalDialog, QtEventListener):
coro = sm.reverse_swap(
transport,
lightning_amount_sat=lightning_amount,
expected_onchain_amount_sat=onchain_amount + self.swap_manager.get_claim_fee(),
expected_onchain_amount_sat=onchain_amount + self.swap_manager.get_swap_tx_fee(),
zeroconf=self.zeroconf,
)
try:

View File

@@ -77,18 +77,20 @@ class HttpSwapServer(Logger, EventListener):
"percentage": sm.percentage,
"minerFees": {
"baseAsset": {
"normal": sm.normal_fee,
"normal": sm.mining_fee,
"reverse": {
"claim": sm.claim_fee,
"lockup": sm.lockup_fee
}
"claim": sm.mining_fee,
"lockup": sm.mining_fee
},
"mining_fee": sm.mining_fee
},
"quoteAsset": {
"normal": sm.normal_fee,
"normal": sm.mining_fee,
"reverse": {
"claim": sm.claim_fee,
"lockup": sm.lockup_fee
}
"claim": sm.mining_fee,
"lockup": sm.mining_fee
},
"mining_fee": sm.mining_fee
}
}
}

View File

@@ -55,8 +55,7 @@ if TYPE_CHECKING:
CLAIM_FEE_SIZE = 136
LOCKUP_FEE_SIZE = 153 # assuming 1 output, 2 outputs
SWAP_TX_SIZE = 150 # default tx size, used for mining fee estimation
MIN_LOCKTIME_DELTA = 60
LOCKTIME_DELTA_REFUND = 70
@@ -130,9 +129,7 @@ def now():
@attr.s
class SwapFees:
percentage = attr.ib(type=int)
normal_fee = attr.ib(type=int)
lockup_fee = attr.ib(type=int)
claim_fee = attr.ib(type=int)
mining_fee = attr.ib(type=int)
min_amount = attr.ib(type=int)
max_amount = attr.ib(type=int)
@@ -176,7 +173,7 @@ def create_claim_tx(
"""
# FIXME the mining fee should depend on swap.is_reverse.
# the txs are not the same size...
amount_sat = txin.value_sats() - SwapManager._get_fee(size=CLAIM_FEE_SIZE, config=config)
amount_sat = txin.value_sats() - SwapManager._get_fee(size=SWAP_TX_SIZE, config=config)
if amount_sat < dust_threshold():
raise BelowDustLimit()
txin, locktime = SwapManager.create_claim_txin(txin=txin, swap=swap, config=config)
@@ -196,9 +193,7 @@ class SwapManager(Logger):
def __init__(self, *, wallet: 'Abstract_Wallet', lnworker: 'LNWallet'):
Logger.__init__(self)
self.normal_fee = None
self.lockup_fee = None
self.claim_fee = None # part of the boltz prococol, not used by Electrum
self.mining_fee = None
self.percentage = None
self._min_amount = None
self._max_amount = None
@@ -417,7 +412,7 @@ class SwapManager(Logger):
else:
claim_tx.add_info_from_wallet(self.wallet)
claim_tx_fee = claim_tx.get_fee()
recommended_fee = self.get_claim_fee()
recommended_fee = self.get_swap_tx_fee()
if claim_tx_fee * 1.1 < recommended_fee:
should_bump_fee = True
self.logger.info(f'claim tx fee too low {claim_tx_fee} < {recommended_fee}. we will bump the fee')
@@ -465,8 +460,8 @@ class SwapManager(Logger):
except TxBroadcastError:
self.logger.info(f'error broadcasting claim tx {txin.spent_txid}')
def get_claim_fee(self):
return self.get_fee(CLAIM_FEE_SIZE)
def get_swap_tx_fee(self):
return self.get_fee(SWAP_TX_SIZE)
def get_fee(self, size):
# note: 'size' is in vbytes
@@ -546,7 +541,7 @@ class SwapManager(Logger):
) -> Tuple[SwapData, str, Optional[str]]:
"""creates a hold invoice"""
if prepay:
prepay_amount_sat = self.get_claim_fee() * 2
prepay_amount_sat = self.get_swap_tx_fee() * 2
invoice_amount_sat = lightning_amount_sat - prepay_amount_sat
else:
invoice_amount_sat = lightning_amount_sat
@@ -965,15 +960,11 @@ class SwapManager(Logger):
self.percentage = float(self.config.SWAPSERVER_FEE_MILLIONTHS) / 10000
self._min_amount = 20000
self._max_amount = 10000000
self.normal_fee = self.get_fee(CLAIM_FEE_SIZE)
self.lockup_fee = self.get_fee(LOCKUP_FEE_SIZE)
self.claim_fee = self.get_fee(CLAIM_FEE_SIZE)
self.mining_fee = self.get_fee(SWAP_TX_SIZE)
def update_pairs(self, pairs):
self.logger.info(f'updating fees {pairs}')
self.normal_fee = pairs.normal_fee
self.lockup_fee = pairs.lockup_fee
self.claim_fee = pairs.claim_fee
self.mining_fee = pairs.mining_fee
self.percentage = pairs.percentage
self._min_amount = pairs.min_amount
self._max_amount = pairs.max_amount
@@ -1006,13 +997,13 @@ class SwapManager(Logger):
# 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
base_fee = self.mining_fee
x -= percentage_fee + base_fee
x = math.floor(x)
if x < dust_threshold():
return
else:
x -= self.normal_fee
x -= self.mining_fee
percentage_fee = math.ceil(x * percentage / (100 + percentage))
x -= percentage_fee
if not self.check_invoice_amount(x):
@@ -1034,7 +1025,7 @@ class SwapManager(Logger):
# 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
base_fee = self.mining_fee
x += base_fee
x = math.ceil(x / ((100 - percentage) / 100))
if not self.check_invoice_amount(x):
@@ -1046,7 +1037,7 @@ class SwapManager(Logger):
# 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 += percentage_fee + self.mining_fee
x = int(x)
return x
@@ -1062,13 +1053,13 @@ class SwapManager(Logger):
f"send_amount={send_amount} -> recv_amount={recv_amount} -> inverted_send_amount={inverted_send_amount}")
# second, add on-chain claim tx fee
if is_reverse and recv_amount is not None:
recv_amount -= self.get_claim_fee()
recv_amount -= self.get_swap_tx_fee()
return recv_amount
def get_send_amount(self, recv_amount: Optional[int], *, is_reverse: bool) -> Optional[int]:
# first, add on-chain claim tx fee
if is_reverse and recv_amount is not None:
recv_amount += self.get_claim_fee()
recv_amount += self.get_swap_tx_fee()
# second, add percentage fee
send_amount = self._get_send_amount(recv_amount, is_reverse=is_reverse)
# sanity check calculation can be inverted
@@ -1310,9 +1301,7 @@ class HttpTransport(SwapServerTransport):
limits = response['pairs']['BTC/BTC']['limits']
pairs = SwapFees(
percentage = fees['percentage'],
normal_fee = fees['minerFees']['baseAsset']['normal'],
lockup_fee = fees['minerFees']['baseAsset']['reverse']['lockup'],
claim_fee = fees['minerFees']['baseAsset']['reverse']['claim'],
mining_fee = fees['minerFees']['baseAsset']['mining_fee'],
min_amount = limits['minimal'],
max_amount = limits['maximal'],
)
@@ -1327,7 +1316,7 @@ class NostrTransport(SwapServerTransport):
NOSTR_DM = 4
USER_STATUS_NIP38 = 30315
NOSTR_EVENT_VERSION = 2
NOSTR_EVENT_VERSION = 3
OFFER_UPDATE_INTERVAL_SEC = 60 * 10
def __init__(self, config, sm, keypair):
@@ -1407,9 +1396,7 @@ class NostrTransport(SwapServerTransport):
def _parse_offer(self, offer):
return SwapFees(
percentage = offer['percentage_fee'],
normal_fee = offer['normal_mining_fee'],
lockup_fee = offer['reverse_mining_fee'],
claim_fee = offer['claim_mining_fee'],
mining_fee = offer['mining_fee'],
min_amount = offer['min_amount'],
max_amount = offer['max_amount'],
)
@@ -1420,9 +1407,7 @@ class NostrTransport(SwapServerTransport):
assert self.sm.is_server
offer = {
'percentage_fee': sm.percentage,
'normal_mining_fee': sm.normal_fee,
'reverse_mining_fee': sm.lockup_fee,
'claim_mining_fee': sm.claim_fee,
'mining_fee': sm.mining_fee,
'min_amount': sm._min_amount,
'max_amount': sm._max_amount,
'relays': sm.config.NOSTR_RELAYS,
@@ -1502,7 +1487,7 @@ class NostrTransport(SwapServerTransport):
await self.taskgroup.spawn(self.rebroadcast_event(event, server_relays))
async def get_pairs(self):
if self.config.SWAPSERVER_NPUB is None:
if not self.config.SWAPSERVER_NPUB:
return
query = {
"kinds": [self.USER_STATUS_NIP38],

View File

@@ -11,6 +11,7 @@ class TestSwapTxs(ElectrumTestCase):
def setUp(self):
super().setUp()
self.maxDiff = None
self.config = SimpleConfig({'electrum_path': self.electrum_path})
self.config.FEE_EST_DYNAMIC = False
self.config.FEE_EST_STATIC_FEERATE = 1000
@@ -41,7 +42,7 @@ class TestSwapTxs(ElectrumTestCase):
config=self.config,
)
self.assertEqual(
"02000000000101f9db8580febd5c0f85b6f1576c83f7739109e3a2d772743e3217e9537fea7e890000000000fdffffff019e07030000000000160014fbfad1ca8741ce640a3ea130bd4478fdd8a2dd8f034730440220156d62534a4e8247eef6bb185c89c4013353c017e45d41ce634976b9d7122c6202202ddb593983fd789cf2166038411425c119d087bc37ec7f8b51bebf603e428fbb0120f1939b5723155713855d7ebea6e174f77d41d669269e7f138856c3de190e7a366a8201208763a914d7a62ef0270960fe23f0f351b28caadab62c21838821030bfd61153816df786036ea293edce851d3a4b9f4a1c66bdc1a17f00ffef3d6b167750334ef24b1752102fc8128f17f9e666ea281c702171ab16c1dd2a4337b71f08970f5aa10c608a93268ac00000000",
"02000000000101f9db8580febd5c0f85b6f1576c83f7739109e3a2d772743e3217e9537fea7e890000000000fdffffff019007030000000000160014fbfad1ca8741ce640a3ea130bd4478fdd8a2dd8f03473044022025506044aba4939f4f2faa94710673ca65530a621f1fa538a3d046dc98bb685e02205f8d463dc6f81e1083f26fa963e581dabc80ea42f8cd59c9e31f3bf531168a9c0120f1939b5723155713855d7ebea6e174f77d41d669269e7f138856c3de190e7a366a8201208763a914d7a62ef0270960fe23f0f351b28caadab62c21838821030bfd61153816df786036ea293edce851d3a4b9f4a1c66bdc1a17f00ffef3d6b167750334ef24b1752102fc8128f17f9e666ea281c702171ab16c1dd2a4337b71f08970f5aa10c608a93268ac00000000",
str(tx)
)
@@ -71,7 +72,7 @@ class TestSwapTxs(ElectrumTestCase):
config=self.config,
)
self.assertEqual(
"0200000000010106871505e5f6dc76f406f38e34e29b54908c6b54da978c28c18fb39ab1dcec080000000000fdffffff0148fb01000000000016001497b4b718e7d06c9c43cd3bcf37905041b718b81f034730440220254e054fc195801aca3d62641a0f27d888f44d1dd66760ae5c3418502e82c141022014305da98daa27d665310115845d2fa6d4dc612d910a186db2624aa558bff9fe010065a914b12bd886ef4fd9ef1c03e899123f2c4b96cec0878763210267ca676c2ed05bb6c380880f1e50b6ef91025dfa963dc49d6c5cb9848f2acf7d670339ef24b1752103d8190cdfcc7dd929a583b7ea8fa8eb1d8463195d336be2f2df94f950ce8b659968ac39ef2400",
"0200000000010106871505e5f6dc76f406f38e34e29b54908c6b54da978c28c18fb39ab1dcec080000000000fdffffff013afb01000000000016001497b4b718e7d06c9c43cd3bcf37905041b718b81f0347304402200ae708af1393f785c541bbc4d7351791b76a53077a292b71cb2a25ad13a15f9902206b7b91c414ec0d6e5098a1acc26de4b47f3aac414b7a49741e8f27cc6a967a19010065a914b12bd886ef4fd9ef1c03e899123f2c4b96cec0878763210267ca676c2ed05bb6c380880f1e50b6ef91025dfa963dc49d6c5cb9848f2acf7d670339ef24b1752103d8190cdfcc7dd929a583b7ea8fa8eb1d8463195d336be2f2df94f950ce8b659968ac39ef2400",
str(tx)
)