Skip to content

Commit 107f15e

Browse files
Mint and borrow (#549)
1 parent 8637b3e commit 107f15e

File tree

15 files changed

+728
-273
lines changed

15 files changed

+728
-273
lines changed

services/vault/src/clients/eth-contract/vault-controller/query.ts

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { type Abi, type Address, type Hex } from "viem";
44

55
import { ethClient } from "../client";
6+
import { normalizeMarketId } from "../morpho/utils";
67
import { executeMulticall } from "../multicall-helpers";
78

89
import BTCVaultControllerABI from "./abis/BTCVaultController.abi.json";
@@ -82,6 +83,104 @@ export async function getUserPositions(
8283
}
8384
}
8485

86+
/**
87+
* Get the position ID (key) for a user in a specific market
88+
* This is a pure function that calculates the position ID from depositor + marketId
89+
*
90+
* @param contractAddress - BTCVaultController contract address
91+
* @param userAddress - User's Ethereum address
92+
* @param marketId - Market ID (bytes32, with or without 0x prefix)
93+
* @returns Position ID (bytes32)
94+
*/
95+
export async function getPositionKey(
96+
contractAddress: Address,
97+
userAddress: Address,
98+
marketId: string | bigint,
99+
): Promise<Hex> {
100+
const publicClient = ethClient.getPublicClient();
101+
102+
// Normalize market ID to ensure it has 0x prefix
103+
const normalizedMarketId = normalizeMarketId(marketId);
104+
105+
const positionId = await publicClient.readContract({
106+
address: contractAddress,
107+
abi: BTCVaultControllerABI,
108+
functionName: "getPositionKey",
109+
args: [userAddress, normalizedMarketId],
110+
});
111+
112+
return positionId as Hex;
113+
}
114+
115+
/**
116+
* Get a single position for a user in a specific market
117+
* This is more efficient than fetching all positions when you only need one
118+
*
119+
* @param contractAddress - BTCVaultController contract address
120+
* @param userAddress - User's Ethereum address
121+
* @param marketId - Market ID (bytes32, with or without 0x prefix)
122+
* @returns Market position or null if position doesn't exist
123+
*/
124+
export async function getPosition(
125+
contractAddress: Address,
126+
userAddress: Address,
127+
marketId: string | bigint,
128+
): Promise<MarketPosition | null> {
129+
try {
130+
const publicClient = ethClient.getPublicClient();
131+
132+
// Normalize market ID to ensure it has 0x prefix
133+
const normalizedMarketId = normalizeMarketId(marketId);
134+
135+
const result = await publicClient.readContract({
136+
address: contractAddress,
137+
abi: BTCVaultControllerABI,
138+
functionName: "getPosition",
139+
args: [userAddress, normalizedMarketId],
140+
});
141+
142+
// Type assertion for the result tuple
143+
type PositionResult = {
144+
depositor: DepositorStruct;
145+
marketId: Hex;
146+
proxyContract: Address;
147+
pegInTxHashes: Hex[];
148+
totalCollateral: bigint;
149+
totalBorrowed: bigint;
150+
lastUpdateTimestamp: bigint;
151+
};
152+
153+
const position = result as PositionResult;
154+
155+
// Check if position exists (proxyContract should not be zero address)
156+
if (
157+
position.proxyContract === "0x0000000000000000000000000000000000000000"
158+
) {
159+
return null;
160+
}
161+
162+
return {
163+
depositor: {
164+
ethAddress: position.depositor.ethAddress as Address,
165+
btcPubKey: position.depositor.btcPubKey as Hex,
166+
},
167+
marketId: position.marketId,
168+
proxyContract: position.proxyContract,
169+
pegInTxHashes: position.pegInTxHashes,
170+
totalCollateral: position.totalCollateral,
171+
totalBorrowed: position.totalBorrowed,
172+
lastUpdateTimestamp: position.lastUpdateTimestamp,
173+
};
174+
} catch (error) {
175+
// Position doesn't exist or error fetching
176+
console.error(
177+
`Failed to get position for user ${userAddress} in market ${marketId}:`,
178+
error,
179+
);
180+
return null;
181+
}
182+
}
183+
85184
/**
86185
* Bulk get position data for multiple position IDs
87186
* Uses multicall to batch requests into a single RPC call for better performance
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
/**
2+
* Hook for borrow transaction handler
3+
* Handles the borrow flow logic and transaction execution
4+
*/
5+
6+
import { parseUnits, type Hex } from "viem";
7+
import { useWalletClient } from "wagmi";
8+
9+
import { BTCVaultsManager } from "../../../../clients/eth-contract";
10+
import { CONTRACTS } from "../../../../config/contracts";
11+
import {
12+
addCollateralWithMarketId,
13+
borrowMoreFromPosition,
14+
} from "../../../../services/position/positionTransactionService";
15+
import { findVaultIndicesForAmount } from "../../../../utils/subsetSum";
16+
17+
interface AvailableVault {
18+
txHash: string;
19+
amountSatoshis: bigint;
20+
}
21+
22+
interface UseBorrowTransactionProps {
23+
hasPosition: boolean;
24+
marketId: string | undefined;
25+
availableVaults: AvailableVault[];
26+
lastBorrowData: {
27+
collateral: number;
28+
borrow: number;
29+
};
30+
refetch: () => Promise<void>;
31+
onBorrowSuccess: () => void;
32+
setProcessing: (processing: boolean) => void;
33+
}
34+
35+
export interface UseBorrowTransactionResult {
36+
handleConfirmBorrow: () => Promise<void>;
37+
}
38+
39+
/**
40+
* Handles borrow transaction logic
41+
*/
42+
export function useBorrowTransaction({
43+
hasPosition,
44+
marketId,
45+
availableVaults,
46+
lastBorrowData,
47+
refetch,
48+
onBorrowSuccess,
49+
setProcessing,
50+
}: UseBorrowTransactionProps): UseBorrowTransactionResult {
51+
const { data: walletClient } = useWalletClient();
52+
const chain = walletClient?.chain;
53+
54+
const handleConfirmBorrow = async () => {
55+
setProcessing(true);
56+
try {
57+
// Validate wallet connection
58+
if (!walletClient || !chain) {
59+
throw new Error("Wallet not connected. Please connect your wallet.");
60+
}
61+
62+
// Validate market ID
63+
if (!marketId) {
64+
throw new Error("Market ID is required for borrowing.");
65+
}
66+
67+
const { collateral: collateralBTC, borrow: borrowUSDC } = lastBorrowData;
68+
69+
// Validate amounts
70+
if (borrowUSDC <= 0) {
71+
throw new Error("Borrow amount must be greater than 0");
72+
}
73+
74+
// Convert borrow amount from USDC to bigint (6 decimals)
75+
const borrowAmountBigint = parseUnits(borrowUSDC.toString(), 6);
76+
77+
if (collateralBTC > 0) {
78+
// Case 1: Add new collateral and borrow
79+
// Convert collateral from BTC to satoshis
80+
const collateralSatoshis = BigInt(Math.round(collateralBTC * 1e8));
81+
82+
// Find which vaults to use for this collateral amount
83+
const vaultAmounts = availableVaults.map((v) => v.amountSatoshis);
84+
const vaultIndices = findVaultIndicesForAmount(
85+
vaultAmounts,
86+
collateralSatoshis,
87+
);
88+
89+
if (!vaultIndices) {
90+
throw new Error(
91+
`Cannot find vault combination for ${collateralBTC} BTC. Please select a different amount.`,
92+
);
93+
}
94+
95+
// Get txHashes for selected vaults
96+
const pegInTxHashes = vaultIndices.map(
97+
(i) => availableVaults[i].txHash as Hex,
98+
);
99+
100+
// Validate vault statuses before attempting to borrow
101+
const vaultStatuses = await Promise.all(
102+
pegInTxHashes.map((txHash) =>
103+
BTCVaultsManager.getPeginRequest(
104+
CONTRACTS.BTC_VAULTS_MANAGER,
105+
txHash,
106+
),
107+
),
108+
);
109+
110+
// Check all vaults are in AVAILABLE status (status 2)
111+
const invalidVaults = vaultStatuses.filter((v) => v.status !== 2);
112+
if (invalidVaults.length > 0) {
113+
const statusNames = invalidVaults.map((v) =>
114+
v.status === 0
115+
? "Pending"
116+
: v.status === 1
117+
? "Verified"
118+
: v.status === 3
119+
? "InPosition"
120+
: "Expired",
121+
);
122+
throw new Error(
123+
`Cannot borrow: ${invalidVaults.length} vault(s) are not in AVAILABLE status. Current statuses: ${statusNames.join(", ")}. Only vaults with AVAILABLE status (status 2) can be used for borrowing.`,
124+
);
125+
}
126+
127+
await addCollateralWithMarketId(
128+
walletClient,
129+
chain,
130+
CONTRACTS.VAULT_CONTROLLER,
131+
pegInTxHashes,
132+
marketId,
133+
borrowAmountBigint,
134+
);
135+
} else {
136+
// Case 2: Borrow more from existing position (no new collateral)
137+
if (!hasPosition) {
138+
throw new Error(
139+
"No existing position found. Please add collateral first.",
140+
);
141+
}
142+
143+
await borrowMoreFromPosition(
144+
walletClient,
145+
chain,
146+
CONTRACTS.VAULT_CONTROLLER,
147+
marketId,
148+
borrowAmountBigint,
149+
);
150+
}
151+
152+
// Refetch position data to update UI
153+
await refetch();
154+
155+
// Success - show success modal
156+
onBorrowSuccess();
157+
} catch (error) {
158+
// Log detailed error information for debugging
159+
console.error("Borrow failed:", error);
160+
console.error("Error details:", {
161+
message: error instanceof Error ? error.message : "Unknown error",
162+
cause: error instanceof Error ? error.cause : undefined,
163+
stack: error instanceof Error ? error.stack : undefined,
164+
fullError: error,
165+
});
166+
167+
// TODO: Show error to user with proper error handling UI
168+
} finally {
169+
setProcessing(false);
170+
}
171+
};
172+
173+
return {
174+
handleConfirmBorrow,
175+
};
176+
}

0 commit comments

Comments
 (0)