mirror of
https://github.com/Routstr/routstr-core.git
synced 2026-10-05 12:28:22 +00:00
improve mint fee calculation
This commit is contained in:
+76
-19
@@ -1,5 +1,4 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import math
|
|
||||||
import time
|
import time
|
||||||
from typing import TypedDict
|
from typing import TypedDict
|
||||||
|
|
||||||
@@ -60,6 +59,76 @@ async def send_token(amount: int, unit: str, mint_url: str | None = None) -> str
|
|||||||
return token
|
return token
|
||||||
|
|
||||||
|
|
||||||
|
async def _calculate_swap_amount(
|
||||||
|
amount_msat: int,
|
||||||
|
token_unit: str,
|
||||||
|
token_mint_url: str,
|
||||||
|
token_wallet: Wallet,
|
||||||
|
primary_wallet: Wallet,
|
||||||
|
) -> int:
|
||||||
|
"""
|
||||||
|
Calculate the amount to mint on the primary mint after accounting for
|
||||||
|
potential swap fees (melt fees) on the foreign mint.
|
||||||
|
"""
|
||||||
|
if settings.primary_mint_unit == "sat":
|
||||||
|
receive_amount = amount_msat // 1000
|
||||||
|
else:
|
||||||
|
receive_amount = amount_msat
|
||||||
|
|
||||||
|
if token_mint_url == settings.primary_mint:
|
||||||
|
logger.info(
|
||||||
|
"swap_to_primary_mint: skipping fee estimation (same mint)",
|
||||||
|
extra={"minted_amount": receive_amount},
|
||||||
|
)
|
||||||
|
return int(receive_amount)
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"swap_to_primary_mint: estimating fees",
|
||||||
|
extra={
|
||||||
|
"dummy_amount": receive_amount,
|
||||||
|
"unit": settings.primary_mint_unit,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
dummy_mint_quote = await primary_wallet.request_mint(receive_amount)
|
||||||
|
dummy_melt_quote = await token_wallet.melt_quote(dummy_mint_quote.request)
|
||||||
|
|
||||||
|
fee_reserve = dummy_melt_quote.fee_reserve
|
||||||
|
if token_unit == "sat":
|
||||||
|
fee_msat = fee_reserve * 1000
|
||||||
|
else:
|
||||||
|
fee_msat = fee_reserve
|
||||||
|
|
||||||
|
amount_msat_after_fee = amount_msat - fee_msat
|
||||||
|
|
||||||
|
if settings.primary_mint_unit == "sat":
|
||||||
|
minted_amount = int(amount_msat_after_fee // 1000)
|
||||||
|
else:
|
||||||
|
minted_amount = int(amount_msat_after_fee)
|
||||||
|
|
||||||
|
if minted_amount <= 0:
|
||||||
|
raise ValueError(f"Fees ({fee_reserve} {token_unit}) exceed token amount")
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"swap_to_primary_mint: fee estimation result",
|
||||||
|
extra={
|
||||||
|
"token_amount_sat": amount_msat // 1000,
|
||||||
|
"estimated_fee_sat": fee_msat // 1000,
|
||||||
|
"minted_amount": minted_amount,
|
||||||
|
"minted_unit": settings.primary_mint_unit,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return minted_amount
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(
|
||||||
|
"swap_to_primary_mint: fee estimation failed",
|
||||||
|
extra={"error": str(e)},
|
||||||
|
)
|
||||||
|
raise ValueError(f"Failed to estimate fees: {e}") from e
|
||||||
|
|
||||||
|
|
||||||
async def swap_to_primary_mint(
|
async def swap_to_primary_mint(
|
||||||
token_obj: Token, token_wallet: Wallet
|
token_obj: Token, token_wallet: Wallet
|
||||||
) -> tuple[int, str, str]:
|
) -> tuple[int, str, str]:
|
||||||
@@ -84,26 +153,14 @@ async def swap_to_primary_mint(
|
|||||||
amount_msat = token_amount
|
amount_msat = token_amount
|
||||||
else:
|
else:
|
||||||
raise ValueError("Invalid unit")
|
raise ValueError("Invalid unit")
|
||||||
if token_obj.mint != settings.primary_mint:
|
|
||||||
estimated_fee_sat = math.ceil(max(amount_msat // 1000 * 0.01, 2)) + 1
|
|
||||||
else:
|
|
||||||
estimated_fee_sat = 0
|
|
||||||
amount_msat_after_fee = amount_msat - estimated_fee_sat * 1000
|
|
||||||
primary_wallet = await get_wallet(settings.primary_mint, settings.primary_mint_unit)
|
primary_wallet = await get_wallet(settings.primary_mint, settings.primary_mint_unit)
|
||||||
|
|
||||||
if settings.primary_mint_unit == "sat":
|
minted_amount = await _calculate_swap_amount(
|
||||||
minted_amount = int(amount_msat_after_fee // 1000)
|
amount_msat,
|
||||||
else:
|
token_obj.unit,
|
||||||
minted_amount = int(amount_msat_after_fee)
|
token_obj.mint,
|
||||||
|
token_wallet,
|
||||||
logger.info(
|
primary_wallet,
|
||||||
"swap_to_primary_mint: fee estimation",
|
|
||||||
extra={
|
|
||||||
"token_amount_sat": amount_msat // 1000,
|
|
||||||
"estimated_fee_sat": estimated_fee_sat,
|
|
||||||
"minted_amount": minted_amount,
|
|
||||||
"minted_unit": settings.primary_mint_unit,
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
mint_quote = await primary_wallet.request_mint(minted_amount)
|
mint_quote = await primary_wallet.request_mint(minted_amount)
|
||||||
|
|||||||
@@ -217,3 +217,77 @@ async def test_recieve_token_untrusted_mint() -> None:
|
|||||||
assert amount == 900
|
assert amount == 900
|
||||||
assert unit == "sat"
|
assert unit == "sat"
|
||||||
assert mint == "http://mint:3338"
|
assert mint == "http://mint:3338"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_swap_to_primary_mint_success() -> None:
|
||||||
|
"""Test successful swap with dynamic fee calculation."""
|
||||||
|
from routstr.wallet import swap_to_primary_mint
|
||||||
|
|
||||||
|
mock_token = Mock()
|
||||||
|
mock_token.mint = "http://foreign:3338"
|
||||||
|
mock_token.unit = "sat"
|
||||||
|
mock_token.amount = 1000
|
||||||
|
mock_token.keysets = ["keyset1"]
|
||||||
|
mock_token.proofs = [{"amount": 1000}]
|
||||||
|
|
||||||
|
mock_token_wallet = Mock()
|
||||||
|
mock_token_wallet.load_mint = AsyncMock()
|
||||||
|
mock_token_wallet.load_proofs = AsyncMock()
|
||||||
|
|
||||||
|
mock_primary_wallet = Mock()
|
||||||
|
mock_primary_wallet.load_mint = AsyncMock()
|
||||||
|
mock_primary_wallet.load_proofs = AsyncMock()
|
||||||
|
|
||||||
|
# Mocks for the estimation phase
|
||||||
|
# 1. request_mint(dummy_amount=1000) -> invoice_dummy
|
||||||
|
# 2. melt_quote(invoice_dummy) -> fee=10
|
||||||
|
|
||||||
|
# Mocks for the execution phase
|
||||||
|
# 3. request_mint(minted_amount=990) -> invoice_real
|
||||||
|
# 4. melt_quote(invoice_real) -> amount=990, fee=10
|
||||||
|
# 5. melt() -> success
|
||||||
|
# 6. mint() -> success
|
||||||
|
|
||||||
|
mock_mint_quote_dummy = Mock(quote="dummy_quote", request="lnbc_dummy")
|
||||||
|
mock_mint_quote_real = Mock(quote="real_quote", request="lnbc_real")
|
||||||
|
|
||||||
|
# side_effect for request_mint to return dummy then real
|
||||||
|
mock_primary_wallet.request_mint = AsyncMock(
|
||||||
|
side_effect=[mock_mint_quote_dummy, mock_mint_quote_real]
|
||||||
|
)
|
||||||
|
|
||||||
|
mock_melt_quote_dummy = Mock(amount=1000, fee_reserve=10)
|
||||||
|
mock_melt_quote_real = Mock(amount=990, fee_reserve=10)
|
||||||
|
|
||||||
|
# side_effect for melt_quote
|
||||||
|
mock_token_wallet.melt_quote = AsyncMock(
|
||||||
|
side_effect=[mock_melt_quote_dummy, mock_melt_quote_real]
|
||||||
|
)
|
||||||
|
|
||||||
|
mock_token_wallet.melt = AsyncMock(return_value="melted_proofs")
|
||||||
|
mock_primary_wallet.mint = AsyncMock(return_value="minted_proofs")
|
||||||
|
|
||||||
|
from routstr.core.settings import settings
|
||||||
|
|
||||||
|
with patch.object(settings, "primary_mint", "http://primary:3338"):
|
||||||
|
with patch.object(settings, "primary_mint_unit", "sat"):
|
||||||
|
with patch("routstr.wallet.get_wallet", return_value=mock_primary_wallet):
|
||||||
|
amount, unit, mint = await swap_to_primary_mint(
|
||||||
|
mock_token, mock_token_wallet
|
||||||
|
)
|
||||||
|
|
||||||
|
assert amount == 990 # 1000 - 10
|
||||||
|
assert unit == "sat"
|
||||||
|
assert mint == "http://primary:3338"
|
||||||
|
|
||||||
|
# Verify call order/counts
|
||||||
|
assert mock_primary_wallet.request_mint.call_count == 2
|
||||||
|
# First call with full amount for estimation
|
||||||
|
mock_primary_wallet.request_mint.assert_any_call(1000)
|
||||||
|
# Second call with calculated amount
|
||||||
|
mock_primary_wallet.request_mint.assert_any_call(990)
|
||||||
|
|
||||||
|
assert mock_token_wallet.melt_quote.call_count == 2
|
||||||
|
assert mock_token_wallet.melt.called
|
||||||
|
assert mock_primary_wallet.mint.called
|
||||||
|
|||||||
Reference in New Issue
Block a user