Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 20 additions & 12 deletions frontend/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />

<!-- ── Primary SEO ──────────────────────────────────────────────────── -->
<title>BridgeKitty | Cross-Chain Swap Aggregator</title>
<meta name="description" content="Compare routes from LI.FI, Squid, deBridge, Relay, and Across in one place. Get the best quote for any cross-chain swap across Ethereum, Base, BNB Chain, Polygon, and Monad." />
<meta name="keywords" content="cross-chain swap, bridge aggregator, DeFi bridge, token swap, crypto swap, multi-chain, LI.FI, Squid Router, deBridge, Relay, Across, USDC, ETH, Ethereum, Base, BNB Chain, Polygon, Monad, MCP, AI agents, BridgeKitty" />
<title>BridgeKitty | BTCFi Swap Aggregator</title>
<meta name="description" content="BTCFi swap aggregator. Compare routes from LI.FI, Squid, deBridge, Relay, Across, Symbiosis, and Meson across Ethereum, Base, BNB Chain, Polygon, Monad, Merlin, Core, Bitlayer, B² Network, Rootstock, and BOB." />
<meta name="keywords" content="BTCFi, Bitcoin DeFi, BTC bridge, cross-chain swap, bridge aggregator, DeFi bridge, token swap, crypto swap, multi-chain, LI.FI, Squid Router, deBridge, Relay, Across, Symbiosis, Meson, BTC, WBTC, cbBTC, BTCB, USDC, USDT, ETH, Ethereum, Base, BNB Chain, Polygon, Monad, Merlin, Core, Bitlayer, B² Network, Rootstock, BOB, MCP, AI agents, BridgeKitty" />
<meta name="author" content="BridgeKitty" />
<meta name="robots" content="index, follow" />
<link rel="canonical" href="https://bridgekitty.persistence.one" />
Expand All @@ -25,18 +25,26 @@
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />

<!-- ── Open Graph (Facebook, Discord, Telegram, etc.) ───────────────── -->
<!-- ── Open Graph (Facebook, Discord, Telegram, WhatsApp, etc.) ─────── -->
<meta property="og:type" content="website" />
<meta property="og:title" content="BridgeKitty | Cross-Chain Swap Aggregator" />
<meta property="og:description" content="Compare routes from LI.FI, Squid, deBridge, Relay, and Across. Get the best quote for any cross-chain swap." />
<meta property="og:title" content="BridgeKitty | BTCFi Swap Aggregator" />
<meta property="og:description" content="BTCFi swap aggregator. Compare routes from LI.FI, Squid, deBridge, Relay, Across, Symbiosis, and Meson. Get the best quote for any BTCFi or cross-chain swap." />
<meta property="og:url" content="https://bridgekitty.persistence.one" />
<meta property="og:site_name" content="BridgeKitty" />
<meta property="og:locale" content="en_US" />
<meta property="og:image" content="https://bridgekitty.persistence.one/preview.png" />
<meta property="og:image:secure_url" content="https://bridgekitty.persistence.one/preview.png" />
<meta property="og:image:type" content="image/png" />
<meta property="og:image:width" content="366" />
<meta property="og:image:height" content="183" />
<meta property="og:image:alt" content="BridgeKitty — BTCFi Swap Aggregator" />

<!-- ── Twitter Card ─────────────────────────────────────────────────── -->
<meta name="twitter:card" content="summary" />
<meta name="twitter:title" content="BridgeKitty | Cross-Chain Swap Aggregator" />
<meta name="twitter:description" content="Compare routes from LI.FI, Squid, deBridge, Relay, and Across. Get the best quote for any cross-chain swap." />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="BridgeKitty | BTCFi Swap Aggregator" />
<meta name="twitter:description" content="BTCFi swap aggregator. Compare routes from LI.FI, Squid, deBridge, Relay, Across, Symbiosis, and Meson. Get the best quote for any BTCFi or cross-chain swap." />
<meta name="twitter:image" content="https://bridgekitty.persistence.one/preview.png" />
<meta name="twitter:image:alt" content="BridgeKitty — BTCFi Swap Aggregator" />

<!-- ── Structured Data (JSON-LD) ────────────────────────────────────── -->
<script type="application/ld+json">
Expand All @@ -45,7 +53,7 @@
"@type": "WebApplication",
"name": "BridgeKitty",
"url": "https://bridgekitty.persistence.one",
"description": "Cross-chain swap aggregator. Compare quotes from LI.FI, Squid Router, deBridge, Relay, and Across across Ethereum, Base, BNB Chain, Polygon, and Monad. MCP server for AI agent cross-chain swaps.",
"description": "BTCFi swap aggregator. Compare quotes from LI.FI, Squid Router, deBridge, Relay, Across, Symbiosis, and Meson across Ethereum, Base, BNB Chain, Polygon, Monad, Merlin, Core, Bitlayer, B² Network, Rootstock, and BOB. MCP server for AI agent cross-chain swaps.",
"applicationCategory": "FinanceApplication",
"operatingSystem": "Web",
"browserRequirements": "Requires a modern web browser with JavaScript enabled",
Expand All @@ -55,8 +63,8 @@
"priceCurrency": "USD"
},
"featureList": [
"Cross-chain token swaps across 5 blockchains",
"Quote comparison from LI.FI, Squid Router, deBridge, Relay, and Across",
"BTCFi and cross-chain token swaps across 11 blockchains",
"Quote comparison from LI.FI, Squid Router, deBridge, Relay, Across, Symbiosis, and Meson",
"Real-time bridge status tracking",
"Transaction history",
"MCP server for AI agent cross-chain swaps"
Expand Down
27 changes: 15 additions & 12 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,8 @@
"tailwindcss": "^3.4.17",
"typescript": "^5.7.3",
"vite": "^6.0.6"
},
"overrides": {
"axios": "^1.15.2"
}
}
Binary file added frontend/public/preview.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion frontend/src/components/LandingView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export function LandingView({ onHumanClick, onAgentClick }: LandingViewProps) {
</span>
</h1>
<p className="hf-hero-sub">
With BridgeKitty, Cross-Chain Swap Aggregator.
With BridgeKitty, BTCFi Swap Aggregator.
</p>
</div>

Expand Down
16 changes: 4 additions & 12 deletions frontend/src/components/WalletConnector.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { useEffect, useMemo } from 'react';
import { LogOut, Wallet2 } from 'lucide-react';
import { usePrivy, useWallets } from '@privy-io/react-auth';
import { resolveApiBaseUrl } from '../lib/apiBaseUrl';

type EthereumProvider = {
request: (args: { method: string; params?: unknown[] }) => Promise<unknown>;
Expand Down Expand Up @@ -79,18 +78,11 @@ export function PrivyWalletConnector({
const walletAddress = activeWallet?.address ?? getWalletAddress(user);

useEffect(() => {
// We deliberately do NOT POST the wallet address to the backend here.
// Connecting a wallet alone shouldn't link the user's address to their
// IP server-side — registration is deferred until the user actually
// commits to a swap (handled in useSwapExecution).
onWalletAddress(walletAddress ?? null);

if (walletAddress) {
const base = resolveApiBaseUrl();
if (base) {
fetch(`${base}/wallets`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ address: walletAddress })
}).catch(() => {});
}
}
}, [walletAddress, onWalletAddress]);

useEffect(() => {
Expand Down
84 changes: 72 additions & 12 deletions frontend/src/hooks/useSwapExecution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,29 @@ export function useSwapExecution(
);
}, []);

/** Tracks wallets we've already registered with the backend in THIS session,
* so a user can swap multiple times without us re-POSTing /wallets each time.
* Registration is deferred until the user is about to bridge — browsing-only
* visitors never get their address registered. */
const registeredWalletsRef = useRef<Set<string>>(new Set());

const ensureWalletRegistered = useCallback(async (address: string) => {
const key = address.toLowerCase();
if (registeredWalletsRef.current.has(key)) return;
registeredWalletsRef.current.add(key);
try {
await fetch(`${API_BASE_URL}/wallets`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ address }),
});
} catch {
// Non-critical — backend can also derive the wallet from /swaps. If the
// POST failed, drop the cache entry so we retry on the next swap.
registeredWalletsRef.current.delete(key);
}
}, []);

const recordSwap = useCallback(async (
txHash: string,
address: string,
Expand Down Expand Up @@ -135,24 +158,46 @@ export function useSwapExecution(
setError('');
setTxStatus(null);

// Privacy: register the wallet with the backend at the moment the user
// commits to bridging — not on first connect. Browsing-only visitors
// never get their address linked to their IP server-side. Best-effort,
// does not block the swap.
void ensureWalletRegistered(walletBridge.address);

// Two-step flow: bridgekitty-backend's /quote returns quote metadata only;
// /execute builds the unsigned tx for the chosen quoteId.
let executed = await executeQuote(bestQuote.id);

// Defence-in-depth: confirm the calldata actually references our wallet.
// The backend already does this server-side, but two layers is fine.
const walletNeedle = walletBridge.address.toLowerCase().replace(/^0x/, '');
const calldata = (executed.transactionRequest.data ?? '').toLowerCase();
if (calldata.length > 10 && walletNeedle && !calldata.includes(walletNeedle)) {
setError(
'Safety check failed: this quote does not appear to route funds to your connected wallet. Refresh quotes and try again.'
);
// Validate the unsigned transaction the backend returned BEFORE any
// wallet prompt. We do NOT do a substring "wallet needle" check on the
// calldata here — it was trivially bypassable (an attacker could pad
// the user's address into trailing bytes while pointing the actual
// recipient slot elsewhere) and provided false confidence. Instead we
// rely on:
// 1. backend-side recipient validation,
// 2. the strict chainId / value / gasLimit checks below,
// 3. the wallet's own confirmation prompt (which renders `to` and
// `value` for the user to inspect).
const txValidationError = validateTransactionRequest(
{ ...executed.transactionRequest, chainId: executed.chainId },
{
expectedChainId: fromChainId,
isNativeSource: isNativeToken(selectedFromToken.address),
requestedAmountWei:
requestedAmountRaw ?? parseUnits(draft.amount, selectedFromToken.decimals),
}
);
if (txValidationError) {
setError(txValidationError);
return;
}

const txValidationError = validateTransactionRequest(executed.transactionRequest);
if (txValidationError) {
setError(txValidationError);
// Approval transaction (when present) must also be on the right chain.
if (
executed.approvalTransaction &&
executed.approvalTransaction.chainId !== fromChainId
) {
setError('Approval transaction chain mismatch — refusing to sign.');
return;
}

Expand Down Expand Up @@ -180,6 +225,21 @@ export function useSwapExecution(
// stale once the approval is mined — re-fetch the bridge tx in that case.
if (executed.needsPostApprovalBuild) {
executed = await executeQuote(bestQuote.id);
// Re-validate: the backend just returned a fresh tx and we must
// not skip the safety checks on the second payload.
const refreshError = validateTransactionRequest(
{ ...executed.transactionRequest, chainId: executed.chainId },
{
expectedChainId: fromChainId,
isNativeSource: isNativeToken(selectedFromToken.address),
requestedAmountWei:
requestedAmountRaw ?? parseUnits(draft.amount, selectedFromToken.decimals),
}
);
if (refreshError) {
setError(refreshError);
return;
}
}
void approvalTxHash;
} else if (!isNativeToken(selectedFromToken.address)) {
Expand Down Expand Up @@ -258,7 +318,7 @@ export function useSwapExecution(
} finally {
setIsExecuting(false);
}
}, [walletBridge, fromChainId, startStatusPolling, recordSwap, onPostSwap]);
}, [walletBridge, fromChainId, startStatusPolling, recordSwap, onPostSwap, ensureWalletRegistered]);

const clearTxStatus = useCallback(() => setTxStatus(null), []);
const clearError = useCallback(() => setError(''), []);
Expand Down
Loading