Skip to content

Commit f997a6e

Browse files
author
Jony Bursztyn
authored
feat: hooks first architecture (#563)
* docs(packages): add vault architecture documentation - Add comprehensive architecture options documentation - Compare Clean Layers, Feature Modules, and Hooks-First approaches - Document decision framework and migration strategies - Choose Hooks-First architecture for implementation * feat(packages): add pure service functions for deposit flow - Create calculations service for deposit fees and UTXO selection - Add validation functions with consistent result format - Implement data transformers for deposit operations - Extract business logic from components into pure functions * feat(packages): implement business logic hooks for deposits - Add main deposit flow orchestration hook - Create validation hook with UTXO and balance checks - Implement transaction creation and submission hook - Separate business logic from UI components using hooks pattern * docs(packages): add migration guide for hooks-first architecture - Provide step-by-step migration instructions - Include code examples for common patterns - Add testing strategy for each layer - Document benefits and next steps for team adoption * fix(packages): resolve TypeScript errors in deposit hooks - Fix undefined type check in useDepositFlow - Remove unused imports and variables - Correct LocalStorageStatus enum values in transformers - Update PeginDisplayLabel types to match state machine * fix(packages): improve parseBtcToSatoshis input validation - Handle empty strings and edge cases properly - Validate decimal point count (prevent multiple decimals) - Handle strings with only decimals (e.g., '.5') - Add regex validation for final satoshi string - Return 0n for invalid inputs instead of throwing * fix(packages): prevent precision loss in formatSatoshisToBtc - Use bigint arithmetic throughout instead of Number conversion - Avoid precision loss for values >= 2^53 - Handle fractional part with string padding and trimming - Maintain correct decimal place handling * docs(packages): add merge architecture compliance report - Analyze code merged from main against new architecture - Verify all changes are compliant with hooks-first pattern - Document positive patterns (error utilities, useBorrowUI) - Identify areas for future migration - Provide integration recommendations * style(packages): fix linter errors in deposit hooks - Run prettier to fix formatting issues - Fix import ordering - Remove unused variables - Standardize quote styles to double quotes * feat(packages): updates * feat(packages): remove files * feat(packages): tests fixes * feat(packages): improvements * feat(packages): linter * feat(packages): revisions * feat(packages): revisions * feat(packages): revisions * feat(packages): revisions * feat(packages): fix pnpm * feat(packages): fix lint * feat(packages): fixes * feat(packages): fixes * feat(packages): lint * feat(packages): lint * feat(packages): lint * feat(packages): lint * feat(packages): switch network * feat(packages): add tests * feat(packages): add tests * feat(packages): lint
1 parent cdca97a commit f997a6e

33 files changed

Lines changed: 4639 additions & 634 deletions

pnpm-lock.yaml

Lines changed: 600 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

services/vault/package.json

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@
1212
"format": "prettier --check .",
1313
"format:fix": "prettier --write .",
1414
"clean": "rm -r node_modules",
15-
"sort-imports": "eslint --fix ."
15+
"sort-imports": "eslint --fix .",
16+
"test": "vitest",
17+
"test:ui": "vitest --ui",
18+
"test:coverage": "vitest --coverage",
19+
"test:run": "vitest run"
1620
},
1721
"dependencies": {
1822
"@babylonlabs-io/babylon-tbv-rust-wasm": "workspace:*",
@@ -75,6 +79,8 @@
7579
"vite": "^6.4.1",
7680
"vite-plugin-environment": "^1.1.3",
7781
"vite-plugin-node-polyfills": "^0.23.0",
78-
"vite-tsconfig-paths": "^5.1.4"
82+
"vite-tsconfig-paths": "^5.1.4",
83+
"vitest": "^1.6.0",
84+
"@vitest/ui": "^1.6.0"
7985
}
8086
}

services/vault/src/components/Overview/Deposits/DepositFormModal/index.tsx

Lines changed: 61 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,10 @@ import {
1010
SubSection,
1111
Text,
1212
} from "@babylonlabs-io/core-ui";
13-
import { useMemo, useState } from "react";
13+
import { useEffect, useMemo } from "react";
1414

15-
import type { VaultProvider } from "../../../../types/vaultProvider";
16-
import {
17-
btcStringToSatoshi,
18-
satoshiToBtcNumber,
19-
} from "../../../../utils/btcConversion";
20-
import { useVaultProviders } from "../hooks/useVaultProviders";
15+
import { useDepositForm } from "@/hooks/deposit/useDepositForm";
16+
import { depositService } from "@/services/deposit";
2117

2218
interface CollateralDepositModalProps {
2319
open: boolean;
@@ -31,70 +27,74 @@ export function CollateralDepositModal({
3127
open,
3228
onClose,
3329
onDeposit,
34-
btcBalance, // Use actual wallet balance
30+
btcBalance: propBtcBalance,
3531
// TODO: Fetch BTC price from oracle service
36-
// The price oracle is available at services/vault/src/clients/eth-contract/oracle
37-
// - Use getOraclePrice(oracleAddress) to get price with 36 decimals
38-
// - Use convertOraclePriceToUSD(oraclePrice) to convert to USD per BTC
39-
// - Oracle address should be exposed via a service layer (not yet implemented)
40-
// - Create a service in services/vault/src/services/oracle/ if it doesn't exist
4132
btcPrice = 97833.68, // Default: ~$97,834 (to match $489,168.43 for 5 BTC)
4233
}: CollateralDepositModalProps) {
43-
const [amount, setAmount] = useState("");
44-
const [selectedProviders, setSelectedProviders] = useState<string[]>([]);
45-
46-
// Fetch real vault providers from API
34+
// Use the new deposit form hook
4735
const {
48-
providers: vaultProviders,
49-
loading: isLoadingProviders,
50-
error: providersError,
51-
} = useVaultProviders();
52-
53-
// Conversion and validation
36+
formData,
37+
setFormData,
38+
errors,
39+
isValid,
40+
btcBalance,
41+
providers,
42+
isLoadingProviders,
43+
amountSats,
44+
// estimatedFees, // TODO: Display estimated fees in UI
45+
validateForm,
46+
resetForm,
47+
} = useDepositForm();
48+
49+
// Use prop balance if provided, otherwise use wallet balance from hook
50+
const displayBalance = propBtcBalance ?? btcBalance;
51+
52+
// Format balance for display
5453
const btcBalanceFormatted = useMemo(() => {
55-
if (btcBalance === undefined || btcBalance === null) return 0;
56-
return satoshiToBtcNumber(btcBalance);
57-
}, [btcBalance]);
58-
const amountNum = useMemo(() => {
59-
const parsed = parseFloat(amount || "0");
60-
return isNaN(parsed) ? 0 : parsed;
61-
}, [amount]);
54+
if (!displayBalance) return 0;
55+
return Number(depositService.formatSatoshisToBtc(displayBalance, 8));
56+
}, [displayBalance]);
6257

58+
// Calculate USD value
6359
const amountUsd = useMemo(() => {
64-
if (!btcPrice || amountNum === 0) return "";
65-
const usdValue = amountNum * btcPrice;
60+
if (!btcPrice || !formData.amountBtc || formData.amountBtc === "0")
61+
return "";
62+
const btcNum = parseFloat(formData.amountBtc);
63+
if (isNaN(btcNum)) return "";
64+
const usdValue = btcNum * btcPrice;
6665
return `$${usdValue.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
67-
}, [amountNum, btcPrice]);
68-
69-
const isValid = amountNum > 0 && selectedProviders.length > 0;
66+
}, [formData.amountBtc, btcPrice]);
7067

7168
// Handler: Toggle provider selection
7269
const handleToggleProvider = (providerId: string) => {
73-
setSelectedProviders((prev) =>
74-
prev.includes(providerId)
75-
? prev.filter((id) => id !== providerId)
76-
: [...prev, providerId],
77-
);
70+
const newProvider =
71+
providerId === formData.selectedProvider ? "" : providerId;
72+
setFormData({
73+
selectedProvider: newProvider,
74+
});
7875
};
7976

8077
// Handler: Amount input change
8178
const handleAmountChange = (e: React.ChangeEvent<HTMLInputElement>) => {
82-
setAmount(e.target.value);
79+
setFormData({ amountBtc: e.target.value });
8380
};
8481

8582
// Handler: Balance click to auto-fill max amount
8683
const handleBalanceClick = () => {
8784
if (btcBalanceFormatted > 0) {
88-
setAmount(btcBalanceFormatted.toString());
85+
const maxAmount = btcBalanceFormatted.toString();
86+
setFormData({ amountBtc: maxAmount });
8987
}
9088
};
9189

9290
// Handler: Deposit button click
9391
const handleDeposit = () => {
94-
if (isValid) {
95-
// Convert BTC string input to satoshis (bigint)
96-
const amountSats = btcStringToSatoshi(amount);
97-
onDeposit(amountSats, selectedProviders);
92+
if (validateForm()) {
93+
// Use amount in satoshis from the hook
94+
onDeposit(
95+
amountSats,
96+
formData.selectedProvider ? [formData.selectedProvider] : [],
97+
);
9898
}
9999
};
100100

@@ -107,11 +107,17 @@ export function CollateralDepositModal({
107107

108108
// Handler: Reset state on close
109109
const handleClose = () => {
110-
setAmount("");
111-
setSelectedProviders([]);
110+
resetForm();
112111
onClose();
113112
};
114113

114+
// Reset form when modal opens
115+
useEffect(() => {
116+
if (open) {
117+
resetForm();
118+
}
119+
}, [open, resetForm]);
120+
115121
return (
116122
<ResponsiveDialog open={open} onClose={handleClose}>
117123
<DialogHeader
@@ -125,7 +131,7 @@ export function CollateralDepositModal({
125131
<div className="flex flex-col gap-2">
126132
<SubSection className="flex w-full flex-col gap-2">
127133
<AmountItem
128-
amount={amount}
134+
amount={formData.amountBtc}
129135
amountUsd={amountUsd}
130136
currencyIcon="/images/btc.png"
131137
currencyName="Bitcoin"
@@ -144,7 +150,7 @@ export function CollateralDepositModal({
144150
onChange={handleAmountChange}
145151
onKeyDown={handleKeyDown}
146152
onMaxClick={handleBalanceClick}
147-
subtitle=""
153+
subtitle={errors.amount ? errors.amount : ""}
148154
/>
149155
</SubSection>
150156
</div>
@@ -170,20 +176,14 @@ export function CollateralDepositModal({
170176
<div className="flex items-center justify-center py-8">
171177
<Loader size={32} className="text-primary-main" />
172178
</div>
173-
) : providersError ? (
174-
<div className="bg-error/10 rounded-lg p-4">
175-
<Text variant="body2" className="text-error text-sm">
176-
Failed to load vault providers. Please try again.
177-
</Text>
178-
</div>
179-
) : vaultProviders.length === 0 ? (
179+
) : providers.length === 0 ? (
180180
<div className="rounded-lg bg-secondary-highlight p-4">
181181
<Text variant="body2" className="text-sm text-accent-secondary">
182182
No vault providers available at this time.
183183
</Text>
184184
</div>
185185
) : (
186-
vaultProviders.map((provider: VaultProvider) => {
186+
providers.map((provider) => {
187187
const shortId =
188188
provider.id.length > 14
189189
? `${provider.id.slice(0, 8)}...${provider.id.slice(-6)}`
@@ -192,7 +192,7 @@ export function CollateralDepositModal({
192192
<ProviderCard
193193
key={provider.id}
194194
id={provider.id}
195-
name={shortId}
195+
name={provider.name || shortId}
196196
icon={
197197
<Text
198198
variant="body2"
@@ -201,7 +201,7 @@ export function CollateralDepositModal({
201201
{provider.id.slice(2, 3).toUpperCase()}
202202
</Text>
203203
}
204-
isSelected={selectedProviders.includes(provider.id)}
204+
isSelected={formData.selectedProvider === provider.id}
205205
onToggle={handleToggleProvider}
206206
/>
207207
);
@@ -213,7 +213,7 @@ export function CollateralDepositModal({
213213

214214
<DialogFooter className="flex items-center justify-between pb-6">
215215
<Text variant="body2" className="text-sm text-accent-secondary">
216-
{selectedProviders.length} Selected
216+
{formData.selectedProvider ? "1 Selected" : "0 Selected"}
217217
</Text>
218218
<Button
219219
variant="contained"

services/vault/src/components/Overview/Deposits/DepositOverview/index.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@ import { useDepositRowPolling } from "../hooks/useDepositRowPolling";
2727
import { usePayoutSignModal } from "../hooks/usePayoutSignModal";
2828
import { PayoutSignModal } from "../PayoutSignModal";
2929
import {
30-
useVaultDepositState,
31-
VaultDepositStep,
32-
} from "../state/VaultDepositState";
30+
DepositStep as DepositStateStep,
31+
useDepositState,
32+
} from "../state/DepositState";
3333
import {
3434
useVaultRedeemState,
3535
VaultRedeemStep,
@@ -335,7 +335,7 @@ export function DepositOverview() {
335335
setBroadcastSuccessOpen(false);
336336
}, []);
337337

338-
const { goToStep: goToDepositStep } = useVaultDepositState();
338+
const { goToStep: goToDepositStep } = useDepositState();
339339
const { goToStep: goToRedeemStep } = useVaultRedeemState();
340340

341341
const handleDeposit = () => {
@@ -344,7 +344,7 @@ export function DepositOverview() {
344344
openWalletModal();
345345
} else {
346346
// Already connected, open deposit modal directly
347-
goToDepositStep(VaultDepositStep.FORM);
347+
goToDepositStep(DepositStateStep.FORM);
348348
}
349349
};
350350

0 commit comments

Comments
 (0)