Skip to content

Commit 495a7a1

Browse files
committed
feat: add BTC ↔ Lightning swaps via Boltz Exchange
- New Boltz API client (lib/swap/boltz.ts) - no API key needed - API routes: /api/swap/boltz (create/estimate) and /api/swap/boltz/[id] (status) - BoltzSwap UI component in web wallet swap tab - Supports both directions: - BTC → Lightning (submarine swap): creates LN invoice, gets BTC deposit address - Lightning → BTC (reverse swap): pays LN invoice, receives on-chain BTC - Live fee estimates, swap status polling, copy-to-clipboard - Non-custodial via Boltz public API
1 parent 16ea668 commit 495a7a1

File tree

5 files changed

+620
-0
lines changed

5 files changed

+620
-0
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/**
2+
* GET /api/swap/boltz/[id] - Check Boltz swap status
3+
*/
4+
import { NextRequest, NextResponse } from 'next/server';
5+
import { getSwapStatus } from '@/lib/swap/boltz';
6+
7+
export async function GET(
8+
request: NextRequest,
9+
{ params }: { params: Promise<{ id: string }> },
10+
) {
11+
try {
12+
const { id } = await params;
13+
const status = await getSwapStatus(id);
14+
return NextResponse.json({ success: true, ...status });
15+
} catch (error) {
16+
return NextResponse.json(
17+
{ success: false, error: error instanceof Error ? error.message : 'Failed to get swap status' },
18+
{ status: 500 },
19+
);
20+
}
21+
}

src/app/api/swap/boltz/route.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/**
2+
* GET /api/swap/boltz - Get Boltz pair info (limits, fees)
3+
* POST /api/swap/boltz - Create a BTC ↔ Lightning swap
4+
*/
5+
import { NextRequest, NextResponse } from 'next/server';
6+
import {
7+
getBoltzPairInfo,
8+
createSwapIn,
9+
createSwapOut,
10+
estimateSwapFee,
11+
} from '@/lib/swap/boltz';
12+
13+
export async function GET() {
14+
try {
15+
const pair = await getBoltzPairInfo();
16+
return NextResponse.json({ success: true, pair });
17+
} catch (error) {
18+
return NextResponse.json(
19+
{ success: false, error: error instanceof Error ? error.message : 'Failed to get pair info' },
20+
{ status: 500 },
21+
);
22+
}
23+
}
24+
25+
export async function POST(request: NextRequest) {
26+
try {
27+
const body = await request.json();
28+
const { direction, invoice, refundAddress, amountSats, claimAddress } = body;
29+
30+
if (direction === 'in') {
31+
// On-chain BTC → Lightning
32+
if (!invoice) {
33+
return NextResponse.json({ success: false, error: 'Lightning invoice required' }, { status: 400 });
34+
}
35+
const swap = await createSwapIn(invoice, refundAddress);
36+
return NextResponse.json({ success: true, swap });
37+
} else if (direction === 'out') {
38+
// Lightning → On-chain BTC
39+
if (!amountSats || !claimAddress) {
40+
return NextResponse.json(
41+
{ success: false, error: 'amountSats and claimAddress required' },
42+
{ status: 400 },
43+
);
44+
}
45+
const swap = await createSwapOut(amountSats, claimAddress);
46+
return NextResponse.json({ success: true, swap });
47+
} else if (direction === 'estimate') {
48+
// Fee estimation
49+
if (!amountSats || !body.swapDirection) {
50+
return NextResponse.json(
51+
{ success: false, error: 'amountSats and swapDirection (in/out) required' },
52+
{ status: 400 },
53+
);
54+
}
55+
const estimate = await estimateSwapFee(body.swapDirection, amountSats);
56+
return NextResponse.json({ success: true, estimate });
57+
} else {
58+
return NextResponse.json(
59+
{ success: false, error: 'direction must be "in", "out", or "estimate"' },
60+
{ status: 400 },
61+
);
62+
}
63+
} catch (error) {
64+
return NextResponse.json(
65+
{ success: false, error: error instanceof Error ? error.message : 'Swap failed' },
66+
{ status: 500 },
67+
);
68+
}
69+
}

src/app/web-wallet/page.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
} from '@/components/web-wallet/TransactionList';
1414
import { SwapForm } from '@/components/web-wallet/SwapForm';
1515
import { SwapHistory, PendingSwaps } from '@/components/web-wallet/SwapHistory';
16+
import { BoltzSwap } from '@/components/web-wallet/BoltzSwap';
1617
import type { WalletChain } from '@/lib/web-wallet/identity';
1718

1819
export default function WebWalletPage() {
@@ -328,6 +329,14 @@ function DashboardView() {
328329
{/* Pending Swaps */}
329330
<PendingSwaps walletId={walletId} />
330331

332+
{/* BTC ↔ Lightning Swap */}
333+
<BoltzSwap
334+
walletId={walletId}
335+
btcAddress={addressMap["BTC"]}
336+
btcBalance={balanceMap["BTC"]?.balance}
337+
lnBalance={balanceMap["LN"]?.balance}
338+
/>
339+
331340
{/* New Swap Form */}
332341
<div>
333342
<div className="mb-4">

0 commit comments

Comments
 (0)