simplify submarine swap onchain fee model to single base fee
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user