mirror of
https://github.com/Routstr/routstr-core.git
synced 2026-07-30 23:36:15 +00:00
Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b76ff62f1c | ||
|
|
3dae03d731 | ||
|
|
8a1f909083 | ||
|
|
f2a0473fb3 | ||
|
|
33d2ef9ef1 | ||
|
|
f1c3b515fe | ||
|
|
74ddc48ff5 |
+76
-16
@@ -1,5 +1,4 @@
|
||||
import asyncio
|
||||
import math
|
||||
import time
|
||||
from typing import TypedDict
|
||||
|
||||
@@ -60,6 +59,76 @@ async def send_token(amount: int, unit: str, mint_url: str | None = None) -> str
|
||||
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(
|
||||
token_obj: Token, token_wallet: Wallet
|
||||
) -> tuple[int, str, str]:
|
||||
@@ -84,23 +153,14 @@ async def swap_to_primary_mint(
|
||||
amount_msat = token_amount
|
||||
else:
|
||||
raise ValueError("Invalid unit")
|
||||
estimated_fee_sat = math.ceil(max(amount_msat // 1000 * 0.01, 2)) + 1
|
||||
amount_msat_after_fee = amount_msat - estimated_fee_sat * 1000
|
||||
primary_wallet = await get_wallet(settings.primary_mint, settings.primary_mint_unit)
|
||||
|
||||
if settings.primary_mint_unit == "sat":
|
||||
minted_amount = int(amount_msat_after_fee // 1000)
|
||||
else:
|
||||
minted_amount = int(amount_msat_after_fee)
|
||||
|
||||
logger.info(
|
||||
"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,
|
||||
},
|
||||
minted_amount = await _calculate_swap_amount(
|
||||
amount_msat,
|
||||
token_obj.unit,
|
||||
token_obj.mint,
|
||||
token_wallet,
|
||||
primary_wallet,
|
||||
)
|
||||
|
||||
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 unit == "sat"
|
||||
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
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import { ModelsPage } from '@/components/models-page';
|
||||
|
||||
export default function ModelPage() {
|
||||
return <ModelsPage />;
|
||||
}
|
||||
+1
-1
@@ -449,7 +449,7 @@ function DashboardInsights({
|
||||
cursor={false}
|
||||
content={
|
||||
<ChartTooltipContent
|
||||
labelFormatter={(_, payload) =>
|
||||
labelFormatter={(_: React.ReactNode, payload) =>
|
||||
String(payload?.[0]?.payload?.type ?? '')
|
||||
}
|
||||
formatter={(value, name) => {
|
||||
|
||||
@@ -40,7 +40,7 @@ const NAV_ITEMS = [
|
||||
{ title: 'Dashboard', url: '/', icon: LayoutDashboardIcon },
|
||||
{ title: 'Balances', url: '/balances', icon: WalletIcon },
|
||||
{ title: 'Logs', url: '/logs', icon: FileTextIcon },
|
||||
{ title: 'Models', url: '/models', icon: DatabaseIcon },
|
||||
{ title: 'Models', url: '/model', icon: DatabaseIcon },
|
||||
{ title: 'Providers', url: '/providers', icon: ServerIcon },
|
||||
{ title: 'Transactions', url: '/transactions', icon: ArrowRightLeftIcon },
|
||||
{ title: 'Settings', url: '/settings', icon: SettingsIcon },
|
||||
|
||||
@@ -58,7 +58,7 @@ const data = {
|
||||
},
|
||||
{
|
||||
title: 'Models',
|
||||
url: '/models',
|
||||
url: '/model',
|
||||
icon: DatabaseIcon,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
|
||||
export default function ModelsPage() {
|
||||
export function ModelsPage() {
|
||||
const [filteredModels, setFilteredModels] = useState<Model[] | undefined>(
|
||||
undefined
|
||||
);
|
||||
@@ -23,7 +23,7 @@ const PAGE_META: Record<string, { title: string; description: string }> = {
|
||||
title: 'System Logs',
|
||||
description: 'Inspect request and application logs.',
|
||||
},
|
||||
'/models': {
|
||||
'/model': {
|
||||
title: 'Models',
|
||||
description: 'Manage model catalog and provider mappings.',
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user