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
18 changes: 17 additions & 1 deletion package-lock.json

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

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@
"dependencies": {
"@morpho-org/blue-sdk": "^5.0.0",
"@morpho-org/blue-sdk-viem": "^4.0.0",
"@tanstack/react-query": "^5.90.2"
"@tanstack/react-query": "^5.90.2",
"usehooks-ts": "^3.1.1"
},
"optionalDependencies": {
"@rollup/rollup-darwin-arm64": "4.52.4"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,49 @@ import type { PropsWithChildren } from "react";
export interface ActivityListProps {
onNewItem?: () => void;
className?: string;
isEmpty?: boolean;
isConnected?: boolean;
}

export function ActivityList({
onNewItem,
className,
children,
isEmpty = false,
isConnected = false,
}: PropsWithChildren<ActivityListProps>) {
// Show empty state when connected but no activities
if (isEmpty && isConnected) {
return (
<Card className={className}>
<div className="flex flex-col items-center justify-center gap-6 py-12 px-4">
<img
src="/mascot-smile-expression-full-body.png"
alt="Babylon mascot"
className="size-[200px]"
/>
<div className="text-center">
<h3 className="text-xl font-semibold text-accent-primary mb-2">
Supply Collateral BTC Trustlessly
</h3>
<p className="text-sm text-accent-secondary max-w-md">
Enter the amount of BTC you want to deposit and select a provider to secure it.
Your deposit will appear here once confirmed.
</p>
</div>
<IconButton
variant="outlined"
size="large"
onClick={onNewItem}
aria-label="Create new item"
>
<PlusIcon />
</IconButton>
</div>
</Card>
);
}

return (
<Card className={className}>
<div className="flex flex-col gap-4">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export interface ActivityCardData {
label: string;
items: ActivityListItemData[];
}[];
warning?: React.ReactNode;
primaryAction?: ActivityCardActionButton;
secondaryActions?: ActivityCardActionButton[];
}
Expand Down Expand Up @@ -72,6 +73,12 @@ export function ActivityCard({ data, className }: ActivityCardProps) {
{data.secondaryActions && data.secondaryActions.length > 0 && (
<ActivityCardActionSection actions={data.secondaryActions} />
)}

{data.warning && (
<div className="mt-3 sm:mt-4">
{data.warning}
</div>
)}
</div>

{data.primaryAction && (
Expand Down
3 changes: 3 additions & 0 deletions routes/vault/src/assets/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
// Asset exports for vault application

// Bitcoin icon as data URI - Orange bitcoin logo
export const bitcoinIcon = "data:image/svg+xml,%3Csvg viewBox='0 0 24 24' fill='%23FF7C2A' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M23.638 14.904c-1.602 6.43-8.113 10.34-14.542 8.736C2.67 22.05-1.244 15.525.362 9.105 1.962 2.67 8.475-1.243 14.9.358c6.43 1.605 10.342 8.115 8.738 14.548v-.002zm-6.35-4.613c.24-1.59-.974-2.45-2.64-3.03l.54-2.153-1.315-.33-.525 2.107c-.345-.087-.705-.167-1.064-.25l.526-2.127-1.32-.33-.54 2.165c-.285-.067-.565-.132-.84-.2l-1.815-.45-.35 1.407s.975.225.955.236c.535.136.63.486.615.766l-1.477 5.92c-.075.166-.24.406-.614.314.015.02-.96-.24-.96-.24l-.66 1.51 1.71.426.93.242-.54 2.19 1.32.327.54-2.17c.36.1.705.19 1.05.273l-.51 2.154 1.32.33.545-2.19c2.24.427 3.93.257 4.64-1.774.57-1.637-.03-2.58-1.217-3.196.854-.193 1.5-.76 1.68-1.93h.01zm-3.01 4.22c-.404 1.64-3.157.75-4.05.53l.72-2.9c.896.23 3.757.67 3.33 2.37zm.41-4.24c-.37 1.49-2.662.735-3.405.55l.654-2.64c.744.18 3.137.524 2.75 2.084v.006z'/%3E%3C/svg%3E";

// USDC icon as data URI - Simple circle design with USDC branding
export const usdcIcon = 'data:image/svg+xml,%3Csvg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg"%3E%3Ccircle cx="20" cy="20" r="20" fill="%232775CA"/%3E%3Cpath d="M24.0001 17.3999C24.0001 15.6399 22.8001 14.7999 20.4001 14.5999V12.3999H18.8001V14.5599C18.3601 14.5599 17.9201 14.5599 17.4801 14.5999V12.3999H15.8801V14.5999C15.5601 14.5999 15.2401 14.6399 14.9201 14.6399H13.2001V16.3599H14.4401C14.8801 16.3599 15.0801 16.5999 15.0801 16.9599V23.0399C15.0801 23.3999 14.8801 23.6399 14.4401 23.6399H13.2001V25.7599L14.8401 25.7999C15.1601 25.7999 15.4801 25.7999 15.8001 25.8399V28.0399H17.4001V25.8799C17.8401 25.8799 18.2801 25.9199 18.7201 25.9199V28.0799H20.3201V25.8799C23.2001 25.7199 24.8001 24.5199 24.8001 22.3599C24.8001 20.7999 24.0001 19.8799 22.5601 19.4799C23.5201 19.0799 24.0001 18.3599 24.0001 17.3999ZM18.7201 21.1999V23.5199C17.7201 23.4399 17.2001 23.3999 16.6801 23.3599V21.1999C17.2001 21.1599 17.7201 21.1199 18.7201 21.1999ZM20.3201 16.7199C21.2401 16.7999 21.7601 16.8399 22.3201 16.9199V19.0399C21.7601 18.9599 21.2401 18.9199 20.3201 18.8399V16.7199ZM20.3201 23.5599V21.1599C21.3201 21.2399 21.8401 21.2799 22.4001 21.3599C22.6001 22.3599 21.8401 23.1999 20.3201 23.5599Z" fill="white"/%3E%3C/svg%3E';

80 changes: 80 additions & 0 deletions routes/vault/src/clients/vault-providers-api/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/**
* Vault Provider API Client
*
* This module handles fetching vault provider data.
* Currently returns mock data, but structured to support GraphQL integration.
*/

export interface VaultProvider {
id: string;
name: string;
icon: string;
apy?: number;
tvl?: string;
description?: string;
}

export interface VaultProvidersResponse {
providers: VaultProvider[];
}

/**
* Fetch vault providers from backend
* TODO: Implement GraphQL query when backend is ready
*
* @returns Promise<VaultProvidersResponse>
*/
export async function getVaultProviders(): Promise<VaultProvidersResponse> {
// TODO: Replace with actual GraphQL endpoint
// Example GraphQL query:
// query GetVaultProviders {
// vaultProviders {
// id
// name
// icon
// apy
// tvl
// description
// }
// }

// Mock data - will be replaced by GraphQL query
const mockProviders: VaultProvider[] = [
{
id: "ironclad",
name: "Ironclad BTC",
icon: "/icons/ironclad.svg",
apy: 8.5,
tvl: "1.2M",
description: "Secure Bitcoin custody with institutional-grade security"
},
{
id: "atlas",
name: "Atlas Custody",
icon: "/icons/atlas.svg",
apy: 7.8,
tvl: "2.5M",
description: "Regulated custody solution for digital assets"
},
{
id: "stonewall",
name: "Stonewall Capital",
icon: "/icons/stonewall.svg",
apy: 9.2,
tvl: "850K",
description: "High-yield Bitcoin vault provider"
},
{
id: "redwood",
name: "Redwood BTC",
icon: "/icons/redwood.svg",
apy: 8.0,
tvl: "1.8M",
description: "Enterprise-grade Bitcoin vault infrastructure"
},
];

return {
providers: mockProviders,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ import {
ActivityCard,
StatusBadge,
ProviderItem,
Warning,
type ActivityCardData,
type ActivityCardDetailItem,
} from "@babylonlabs-io/core-ui";
import type { VaultActivity } from "../../mockData/vaultActivities";
import { getVaultState, getActionForState } from "../../utils/vaultState";
import { formatUSDCAmount } from "../../utils/peginTransformers";
import { bitcoinIcon } from "../../assets";

interface VaultActivityCardProps {
activity: VaultActivity;
Expand Down Expand Up @@ -114,10 +116,17 @@ export function VaultActivityCard({ activity, onBorrow, onRepay }: VaultActivity
// Transform to ActivityCardData format
const cardData: ActivityCardData = {
formattedAmount: `${activity.collateral.amount} ${activity.collateral.symbol}`,
icon: activity.collateral.icon,
icon: activity.collateral.icon || bitcoinIcon,
iconAlt: activity.collateral.symbol,
details,
optionalDetails: optionalDetails.length > 0 ? optionalDetails : undefined,
// Add warning for pending peg-ins
warning: activity.isPending ? (
<Warning>
{activity.pendingMessage ||
"Your peg-in is being processed. This can take up to ~5 hours while Bitcoin confirmations and provider acknowledgements complete."}
</Warning>
) : undefined,
primaryAction: getActionForState(vaultState, activity, onBorrow, onRepay),
};

Expand Down
92 changes: 84 additions & 8 deletions routes/vault/src/components/VaultDashboard/VaultDashboard.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,30 @@
import { ActivityList } from "@babylonlabs-io/core-ui";
import { useCallback } from "react";
import { BorrowFlow } from "../BorrowFlow";
import { RepayFlow } from "../RepayFlow";
import {
PeginModal,
PeginSignModal,
PeginSuccessModal,
} from "../modals";
import { useVaultPositions } from "../../hooks/useVaultPositions";
import { useBorrowFlow } from "../../hooks/useBorrowFlow";
import { useRepayFlow } from "../../hooks/useRepayFlow";
import { usePeginFlow } from "../../hooks/usePeginFlow";
import { EmptyState } from "./EmptyState";
import { VaultActivityCard } from "./VaultActivityCard";
import type { VaultActivity } from "../../mockData/vaultActivities";

export function VaultDashboard() {
// Data fetching
const { activities, isWalletConnected, refetchActivities } = useVaultPositions();
const {
activities,
isWalletConnected,
refetchActivities,
connectedAddress,
btcAddress,
addPendingPegin,
} = useVaultPositions();

// Borrow flow modal state
const {
Expand All @@ -27,12 +42,47 @@ export function VaultDashboard() {
closeRepayFlow,
} = useRepayFlow();

// Handle "New Item" button click - open borrow for first activity
const handleNewBorrow = () => {
if (activities.length > 0) {
openBorrowFlow(activities[0]);
// Peg-in flow modal state
const {
isOpen: peginFlowOpen,
signModalOpen: peginSignModalOpen,
successModalOpen: peginSuccessModalOpen,
peginAmount,
selectedProviders,
btcBalanceSat,
openPeginFlow,
closePeginFlow,
handlePeginClick,
handlePeginSignSuccess: handlePeginSignSuccessBase,
handlePeginSuccessClose,
} = usePeginFlow();

// Handle peg-in sign success with storage integration
const handlePeginSignSuccess = useCallback(() => {
// Add to local storage when peg-in is submitted
if (connectedAddress && btcAddress) {
const peginId = `pending-${Date.now()}`; // Temporary ID until we get BTC tx hash

addPendingPegin({
id: peginId,
amount: peginAmount.toString(),
providers: selectedProviders,
ethAddress: connectedAddress,
btcAddress: btcAddress,
});

console.log('[VaultDashboard] Added pending peg-in to localStorage:', {
id: peginId,
amount: peginAmount,
providers: selectedProviders,
});
}
};

// Complete the peg-in flow and refetch activities
handlePeginSignSuccessBase(() => {
refetchActivities();
});
}, [connectedAddress, btcAddress, peginAmount, selectedProviders, addPendingPegin, handlePeginSignSuccessBase, refetchActivities]);

// Show message if wallet is not connected
if (!isWalletConnected) {
Expand All @@ -42,8 +92,12 @@ export function VaultDashboard() {
return (
<>
<div className="container mx-auto flex max-w-[760px] flex-1 flex-col px-4 py-8">
<ActivityList onNewItem={handleNewBorrow}>
{activities.map((activity) => (
<ActivityList
onNewItem={openPeginFlow}
isEmpty={activities.length === 0}
isConnected={isWalletConnected}
>
{activities.map((activity: VaultActivity) => (
<VaultActivityCard
key={activity.id}
activity={activity}
Expand All @@ -54,6 +108,28 @@ export function VaultDashboard() {
</ActivityList>
</div>

{/* Peg-in Modals */}
<PeginModal
open={peginFlowOpen}
onClose={closePeginFlow}
onPegIn={handlePeginClick}
btcBalance={btcBalanceSat}
/>

<PeginSignModal
open={peginSignModalOpen}
onClose={() => {}}
onSuccess={handlePeginSignSuccess}
amount={peginAmount}
selectedProviders={selectedProviders}
/>

<PeginSuccessModal
open={peginSuccessModalOpen}
onClose={handlePeginSuccessClose}
amount={peginAmount}
/>

<BorrowFlow
activity={selectedBorrowActivity}
isOpen={borrowFlowOpen}
Expand Down
Loading