Skip to content

Commit ea4adbb

Browse files
authored
Merge pull request spesmilo#9573 from f321x/simplify_subswap_fee
Simplify submarine swap onchain fee model to single base fee 'mining_fee'
2 parents d714ef1 + 6f97b7b commit ea4adbb

File tree

7 files changed

+43
-59
lines changed

7 files changed

+43
-59
lines changed

electrum/commands.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1418,7 +1418,7 @@ async def reverse_swap(self, lightning_amount, onchain_amount, password=None, wa
14181418
funding_txid = None
14191419
else:
14201420
lightning_amount_sat = satoshis(lightning_amount)
1421-
claim_fee = sm.get_claim_fee()
1421+
claim_fee = sm.get_swap_tx_fee()
14221422
onchain_amount_sat = satoshis(onchain_amount) + claim_fee
14231423
funding_txid = await wallet.lnworker.swap_manager.reverse_swap(
14241424
transport,

electrum/gui/qml/qeswaphelper.py

+4-8
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,7 @@ class QESwapServerNPubListModel(QAbstractListModel):
2929
_logger = get_logger(__name__)
3030

3131
# define listmodel rolemap
32-
_ROLE_NAMES= ('npub', 'timestamp', 'percentage_fee', 'normal_mining_fee', 'reverse_mining_fee', 'claim_mining_fee',
33-
'min_amount', 'max_amount')
32+
_ROLE_NAMES= ('npub', 'timestamp', 'percentage_fee', 'mining_fee', 'min_amount', 'max_amount')
3433
_ROLE_KEYS = range(Qt.ItemDataRole.UserRole, Qt.ItemDataRole.UserRole + len(_ROLE_NAMES))
3534
_ROLE_MAP = dict(zip(_ROLE_KEYS, [bytearray(x.encode()) for x in _ROLE_NAMES]))
3635

@@ -69,9 +68,7 @@ def initModel(self, items):
6968
self._services = [{
7069
'npub': x['pubkey'],
7170
'percentage_fee': x['percentage_fee'],
72-
'normal_mining_fee': x['normal_mining_fee'],
73-
'reverse_mining_fee': x['reverse_mining_fee'],
74-
'claim_mining_fee': x['claim_mining_fee'],
71+
'mining_fee': x['mining_fee'],
7572
'min_amount': x['min_amount'],
7673
'max_amount': x['max_amount'],
7774
'timestamp': age(x['timestamp']),
@@ -378,7 +375,6 @@ async def wait_initialized():
378375
self._logger.error(str(e))
379376
except RuntimeError:
380377
pass
381-
382378
if isinstance(transport, NostrTransport):
383379
if not swap_manager.is_initialized.is_set():
384380
if not transport.is_connected.is_set():
@@ -502,7 +498,7 @@ def swap_slider_moved(self):
502498
self.toreceive = QEAmount(amount_sat=self._receive_amount)
503499
# fee breakdown
504500
self.serverfeeperc = f'{swap_manager.percentage:0.1f}%'
505-
server_miningfee = swap_manager.lockup_fee if self.isReverse else swap_manager.normal_fee
501+
server_miningfee = swap_manager.mining_fee
506502
self.serverMiningfee = QEAmount(amount_sat=server_miningfee)
507503
if self.isReverse:
508504
self.check_valid(self._send_amount, self._receive_amount)
@@ -626,7 +622,7 @@ async def coro():
626622
return await swap_manager.reverse_swap(
627623
transport,
628624
lightning_amount_sat=lightning_amount,
629-
expected_onchain_amount_sat=onchain_amount + swap_manager.get_claim_fee(),
625+
expected_onchain_amount_sat=onchain_amount + swap_manager.get_swap_tx_fee(),
630626
)
631627
try:
632628
fut = asyncio.run_coroutine_threadsafe(coro(), loop)

electrum/gui/qt/main_window.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1247,7 +1247,7 @@ def choose_swapserver_dialog(self, transport: NostrTransport) -> bool:
12471247
def descr(x):
12481248
last_seen = util.age(x['timestamp'])
12491249
return (f"pubkey={x['pubkey'][0:10]}, "
1250-
f"fee={x['percentage_fee']}% + {x['reverse_mining_fee']} sats, "
1250+
f"fee={x['percentage_fee']}% + {x['mining_fee']} sats, "
12511251
f"last_seen: {last_seen}")
12521252
server_keys = [(x['pubkey'], descr(x)) for x in recent_offers]
12531253
msg = '\n'.join([

electrum/gui/qt/swap_dialog.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@ def update(self):
239239
self.recv_label.setIcon(recv_icon)
240240
self.description_label.setText(self.get_description())
241241
self.description_label.repaint() # macOS hack for #6269
242-
server_mining_fee = sm.lockup_fee if self.is_reverse else sm.normal_fee
242+
server_mining_fee = sm.mining_fee
243243
server_fee_str = '%.2f'%sm.percentage + '% + ' + self.window.format_amount(server_mining_fee) + ' ' + self.window.base_unit()
244244
self.server_fee_label.setText(server_fee_str)
245245
self.server_fee_label.repaint() # macOS hack for #6269
@@ -249,7 +249,7 @@ def update_fee(self, tx: Optional[PartialTransaction]) -> None:
249249
"""Updates self.fee_label. No other side-effects."""
250250
if self.is_reverse:
251251
sm = self.swap_manager
252-
fee = sm.get_claim_fee()
252+
fee = sm.get_swap_tx_fee()
253253
else:
254254
fee = tx.get_fee() if tx else None
255255
fee_text = self.window.format_amount(fee) + ' ' + self.window.base_unit() if fee else ''
@@ -269,7 +269,7 @@ def run(self, transport):
269269
coro = sm.reverse_swap(
270270
transport,
271271
lightning_amount_sat=lightning_amount,
272-
expected_onchain_amount_sat=onchain_amount + self.swap_manager.get_claim_fee(),
272+
expected_onchain_amount_sat=onchain_amount + self.swap_manager.get_swap_tx_fee(),
273273
zeroconf=self.zeroconf,
274274
)
275275
try:

electrum/plugins/swapserver/server.py

+10-8
Original file line numberDiff line numberDiff line change
@@ -77,18 +77,20 @@ async def get_pairs(self, r):
7777
"percentage": sm.percentage,
7878
"minerFees": {
7979
"baseAsset": {
80-
"normal": sm.normal_fee,
80+
"normal": sm.mining_fee,
8181
"reverse": {
82-
"claim": sm.claim_fee,
83-
"lockup": sm.lockup_fee
84-
}
82+
"claim": sm.mining_fee,
83+
"lockup": sm.mining_fee
84+
},
85+
"mining_fee": sm.mining_fee
8586
},
8687
"quoteAsset": {
87-
"normal": sm.normal_fee,
88+
"normal": sm.mining_fee,
8889
"reverse": {
89-
"claim": sm.claim_fee,
90-
"lockup": sm.lockup_fee
91-
}
90+
"claim": sm.mining_fee,
91+
"lockup": sm.mining_fee
92+
},
93+
"mining_fee": sm.mining_fee
9294
}
9395
}
9496
}

electrum/submarine_swaps.py

+21-36
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,7 @@
5555

5656

5757

58-
CLAIM_FEE_SIZE = 136
59-
LOCKUP_FEE_SIZE = 153 # assuming 1 output, 2 outputs
58+
SWAP_TX_SIZE = 150 # default tx size, used for mining fee estimation
6059

6160
MIN_LOCKTIME_DELTA = 60
6261
LOCKTIME_DELTA_REFUND = 70
@@ -130,9 +129,7 @@ def now():
130129
@attr.s
131130
class SwapFees:
132131
percentage = attr.ib(type=int)
133-
normal_fee = attr.ib(type=int)
134-
lockup_fee = attr.ib(type=int)
135-
claim_fee = attr.ib(type=int)
132+
mining_fee = attr.ib(type=int)
136133
min_amount = attr.ib(type=int)
137134
max_amount = attr.ib(type=int)
138135

@@ -176,7 +173,7 @@ def create_claim_tx(
176173
"""
177174
# FIXME the mining fee should depend on swap.is_reverse.
178175
# the txs are not the same size...
179-
amount_sat = txin.value_sats() - SwapManager._get_fee(size=CLAIM_FEE_SIZE, config=config)
176+
amount_sat = txin.value_sats() - SwapManager._get_fee(size=SWAP_TX_SIZE, config=config)
180177
if amount_sat < dust_threshold():
181178
raise BelowDustLimit()
182179
txin, locktime = SwapManager.create_claim_txin(txin=txin, swap=swap, config=config)
@@ -196,9 +193,7 @@ class SwapManager(Logger):
196193

197194
def __init__(self, *, wallet: 'Abstract_Wallet', lnworker: 'LNWallet'):
198195
Logger.__init__(self)
199-
self.normal_fee = None
200-
self.lockup_fee = None
201-
self.claim_fee = None # part of the boltz prococol, not used by Electrum
196+
self.mining_fee = None
202197
self.percentage = None
203198
self._min_amount = None
204199
self._max_amount = None
@@ -417,7 +412,7 @@ async def _claim_swap(self, swap: SwapData) -> None:
417412
else:
418413
claim_tx.add_info_from_wallet(self.wallet)
419414
claim_tx_fee = claim_tx.get_fee()
420-
recommended_fee = self.get_claim_fee()
415+
recommended_fee = self.get_swap_tx_fee()
421416
if claim_tx_fee * 1.1 < recommended_fee:
422417
should_bump_fee = True
423418
self.logger.info(f'claim tx fee too low {claim_tx_fee} < {recommended_fee}. we will bump the fee')
@@ -465,8 +460,8 @@ async def _claim_swap(self, swap: SwapData) -> None:
465460
except TxBroadcastError:
466461
self.logger.info(f'error broadcasting claim tx {txin.spent_txid}')
467462

468-
def get_claim_fee(self):
469-
return self.get_fee(CLAIM_FEE_SIZE)
463+
def get_swap_tx_fee(self):
464+
return self.get_fee(SWAP_TX_SIZE)
470465

471466
def get_fee(self, size):
472467
# note: 'size' is in vbytes
@@ -546,7 +541,7 @@ def add_normal_swap(
546541
) -> Tuple[SwapData, str, Optional[str]]:
547542
"""creates a hold invoice"""
548543
if prepay:
549-
prepay_amount_sat = self.get_claim_fee() * 2
544+
prepay_amount_sat = self.get_swap_tx_fee() * 2
550545
invoice_amount_sat = lightning_amount_sat - prepay_amount_sat
551546
else:
552547
invoice_amount_sat = lightning_amount_sat
@@ -965,15 +960,11 @@ def server_update_pairs(self) -> None:
965960
self.percentage = float(self.config.SWAPSERVER_FEE_MILLIONTHS) / 10000
966961
self._min_amount = 20000
967962
self._max_amount = 10000000
968-
self.normal_fee = self.get_fee(CLAIM_FEE_SIZE)
969-
self.lockup_fee = self.get_fee(LOCKUP_FEE_SIZE)
970-
self.claim_fee = self.get_fee(CLAIM_FEE_SIZE)
963+
self.mining_fee = self.get_fee(SWAP_TX_SIZE)
971964

972965
def update_pairs(self, pairs):
973966
self.logger.info(f'updating fees {pairs}')
974-
self.normal_fee = pairs.normal_fee
975-
self.lockup_fee = pairs.lockup_fee
976-
self.claim_fee = pairs.claim_fee
967+
self.mining_fee = pairs.mining_fee
977968
self.percentage = pairs.percentage
978969
self._min_amount = pairs.min_amount
979970
self._max_amount = pairs.max_amount
@@ -1006,13 +997,13 @@ def _get_recv_amount(self, send_amount: Optional[int], *, is_reverse: bool) -> O
1006997
# see/ref:
1007998
# https://github.com/BoltzExchange/boltz-backend/blob/e7e2d30f42a5bea3665b164feb85f84c64d86658/lib/service/Service.ts#L948
1008999
percentage_fee = math.ceil(percentage * x / 100)
1009-
base_fee = self.lockup_fee
1000+
base_fee = self.mining_fee
10101001
x -= percentage_fee + base_fee
10111002
x = math.floor(x)
10121003
if x < dust_threshold():
10131004
return
10141005
else:
1015-
x -= self.normal_fee
1006+
x -= self.mining_fee
10161007
percentage_fee = math.ceil(x * percentage / (100 + percentage))
10171008
x -= percentage_fee
10181009
if not self.check_invoice_amount(x):
@@ -1034,7 +1025,7 @@ def _get_send_amount(self, recv_amount: Optional[int], *, is_reverse: bool) -> O
10341025
# see/ref:
10351026
# https://github.com/BoltzExchange/boltz-backend/blob/e7e2d30f42a5bea3665b164feb85f84c64d86658/lib/service/Service.ts#L928
10361027
# https://github.com/BoltzExchange/boltz-backend/blob/e7e2d30f42a5bea3665b164feb85f84c64d86658/lib/service/Service.ts#L958
1037-
base_fee = self.lockup_fee
1028+
base_fee = self.mining_fee
10381029
x += base_fee
10391030
x = math.ceil(x / ((100 - percentage) / 100))
10401031
if not self.check_invoice_amount(x):
@@ -1046,7 +1037,7 @@ def _get_send_amount(self, recv_amount: Optional[int], *, is_reverse: bool) -> O
10461037
# https://github.com/BoltzExchange/boltz-backend/blob/e7e2d30f42a5bea3665b164feb85f84c64d86658/lib/service/Service.ts#L708
10471038
# https://github.com/BoltzExchange/boltz-backend/blob/e7e2d30f42a5bea3665b164feb85f84c64d86658/lib/rates/FeeProvider.ts#L90
10481039
percentage_fee = math.ceil(percentage * x / 100)
1049-
x += percentage_fee + self.normal_fee
1040+
x += percentage_fee + self.mining_fee
10501041
x = int(x)
10511042
return x
10521043

@@ -1062,13 +1053,13 @@ def get_recv_amount(self, send_amount: Optional[int], *, is_reverse: bool) -> Op
10621053
f"send_amount={send_amount} -> recv_amount={recv_amount} -> inverted_send_amount={inverted_send_amount}")
10631054
# second, add on-chain claim tx fee
10641055
if is_reverse and recv_amount is not None:
1065-
recv_amount -= self.get_claim_fee()
1056+
recv_amount -= self.get_swap_tx_fee()
10661057
return recv_amount
10671058

10681059
def get_send_amount(self, recv_amount: Optional[int], *, is_reverse: bool) -> Optional[int]:
10691060
# first, add on-chain claim tx fee
10701061
if is_reverse and recv_amount is not None:
1071-
recv_amount += self.get_claim_fee()
1062+
recv_amount += self.get_swap_tx_fee()
10721063
# second, add percentage fee
10731064
send_amount = self._get_send_amount(recv_amount, is_reverse=is_reverse)
10741065
# sanity check calculation can be inverted
@@ -1310,9 +1301,7 @@ async def get_pairs(self) -> None:
13101301
limits = response['pairs']['BTC/BTC']['limits']
13111302
pairs = SwapFees(
13121303
percentage = fees['percentage'],
1313-
normal_fee = fees['minerFees']['baseAsset']['normal'],
1314-
lockup_fee = fees['minerFees']['baseAsset']['reverse']['lockup'],
1315-
claim_fee = fees['minerFees']['baseAsset']['reverse']['claim'],
1304+
mining_fee = fees['minerFees']['baseAsset']['mining_fee'],
13161305
min_amount = limits['minimal'],
13171306
max_amount = limits['maximal'],
13181307
)
@@ -1327,7 +1316,7 @@ class NostrTransport(SwapServerTransport):
13271316

13281317
NOSTR_DM = 4
13291318
USER_STATUS_NIP38 = 30315
1330-
NOSTR_EVENT_VERSION = 2
1319+
NOSTR_EVENT_VERSION = 3
13311320
OFFER_UPDATE_INTERVAL_SEC = 60 * 10
13321321

13331322
def __init__(self, config, sm, keypair):
@@ -1407,9 +1396,7 @@ def get_recent_offers(self) -> Sequence[Dict]:
14071396
def _parse_offer(self, offer):
14081397
return SwapFees(
14091398
percentage = offer['percentage_fee'],
1410-
normal_fee = offer['normal_mining_fee'],
1411-
lockup_fee = offer['reverse_mining_fee'],
1412-
claim_fee = offer['claim_mining_fee'],
1399+
mining_fee = offer['mining_fee'],
14131400
min_amount = offer['min_amount'],
14141401
max_amount = offer['max_amount'],
14151402
)
@@ -1420,9 +1407,7 @@ async def publish_offer(self, sm):
14201407
assert self.sm.is_server
14211408
offer = {
14221409
'percentage_fee': sm.percentage,
1423-
'normal_mining_fee': sm.normal_fee,
1424-
'reverse_mining_fee': sm.lockup_fee,
1425-
'claim_mining_fee': sm.claim_fee,
1410+
'mining_fee': sm.mining_fee,
14261411
'min_amount': sm._min_amount,
14271412
'max_amount': sm._max_amount,
14281413
'relays': sm.config.NOSTR_RELAYS,
@@ -1502,7 +1487,7 @@ async def receive_offers(self):
15021487
await self.taskgroup.spawn(self.rebroadcast_event(event, server_relays))
15031488

15041489
async def get_pairs(self):
1505-
if self.config.SWAPSERVER_NPUB is None:
1490+
if not self.config.SWAPSERVER_NPUB:
15061491
return
15071492
query = {
15081493
"kinds": [self.USER_STATUS_NIP38],

tests/test_sswaps.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ class TestSwapTxs(ElectrumTestCase):
1111

1212
def setUp(self):
1313
super().setUp()
14+
self.maxDiff = None
1415
self.config = SimpleConfig({'electrum_path': self.electrum_path})
1516
self.config.FEE_EST_DYNAMIC = False
1617
self.config.FEE_EST_STATIC_FEERATE = 1000
@@ -41,7 +42,7 @@ def test_claim_tx_for_successful_reverse_swap(self):
4142
config=self.config,
4243
)
4344
self.assertEqual(
44-
"02000000000101f9db8580febd5c0f85b6f1576c83f7739109e3a2d772743e3217e9537fea7e890000000000fdffffff019e07030000000000160014fbfad1ca8741ce640a3ea130bd4478fdd8a2dd8f034730440220156d62534a4e8247eef6bb185c89c4013353c017e45d41ce634976b9d7122c6202202ddb593983fd789cf2166038411425c119d087bc37ec7f8b51bebf603e428fbb0120f1939b5723155713855d7ebea6e174f77d41d669269e7f138856c3de190e7a366a8201208763a914d7a62ef0270960fe23f0f351b28caadab62c21838821030bfd61153816df786036ea293edce851d3a4b9f4a1c66bdc1a17f00ffef3d6b167750334ef24b1752102fc8128f17f9e666ea281c702171ab16c1dd2a4337b71f08970f5aa10c608a93268ac00000000",
45+
"02000000000101f9db8580febd5c0f85b6f1576c83f7739109e3a2d772743e3217e9537fea7e890000000000fdffffff019007030000000000160014fbfad1ca8741ce640a3ea130bd4478fdd8a2dd8f03473044022025506044aba4939f4f2faa94710673ca65530a621f1fa538a3d046dc98bb685e02205f8d463dc6f81e1083f26fa963e581dabc80ea42f8cd59c9e31f3bf531168a9c0120f1939b5723155713855d7ebea6e174f77d41d669269e7f138856c3de190e7a366a8201208763a914d7a62ef0270960fe23f0f351b28caadab62c21838821030bfd61153816df786036ea293edce851d3a4b9f4a1c66bdc1a17f00ffef3d6b167750334ef24b1752102fc8128f17f9e666ea281c702171ab16c1dd2a4337b71f08970f5aa10c608a93268ac00000000",
4546
str(tx)
4647
)
4748

@@ -71,7 +72,7 @@ def test_claim_tx_for_timing_out_forward_swap(self):
7172
config=self.config,
7273
)
7374
self.assertEqual(
74-
"0200000000010106871505e5f6dc76f406f38e34e29b54908c6b54da978c28c18fb39ab1dcec080000000000fdffffff0148fb01000000000016001497b4b718e7d06c9c43cd3bcf37905041b718b81f034730440220254e054fc195801aca3d62641a0f27d888f44d1dd66760ae5c3418502e82c141022014305da98daa27d665310115845d2fa6d4dc612d910a186db2624aa558bff9fe010065a914b12bd886ef4fd9ef1c03e899123f2c4b96cec0878763210267ca676c2ed05bb6c380880f1e50b6ef91025dfa963dc49d6c5cb9848f2acf7d670339ef24b1752103d8190cdfcc7dd929a583b7ea8fa8eb1d8463195d336be2f2df94f950ce8b659968ac39ef2400",
75+
"0200000000010106871505e5f6dc76f406f38e34e29b54908c6b54da978c28c18fb39ab1dcec080000000000fdffffff013afb01000000000016001497b4b718e7d06c9c43cd3bcf37905041b718b81f0347304402200ae708af1393f785c541bbc4d7351791b76a53077a292b71cb2a25ad13a15f9902206b7b91c414ec0d6e5098a1acc26de4b47f3aac414b7a49741e8f27cc6a967a19010065a914b12bd886ef4fd9ef1c03e899123f2c4b96cec0878763210267ca676c2ed05bb6c380880f1e50b6ef91025dfa963dc49d6c5cb9848f2acf7d670339ef24b1752103d8190cdfcc7dd929a583b7ea8fa8eb1d8463195d336be2f2df94f950ce8b659968ac39ef2400",
7576
str(tx)
7677
)
7778

0 commit comments

Comments
 (0)