mirror of
https://github.com/luxfhe/research-ocp-fhe.git
synced 2026-11-03 06:05:38 +00:00
301 lines
8.9 KiB
TypeScript
301 lines
8.9 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
import { useWriteContract, useWaitForTransactionReceipt } from 'wagmi';
|
|
import { useAccount } from '@/hooks/wagmi-viem-proxy/use-account';
|
|
import { useFhevm } from './use-fhevm';
|
|
import { privateStockFacetAbi } from '@/lib/abi/privateStockFacetAbi';
|
|
import {
|
|
generateBytes16Id,
|
|
generateRoundId,
|
|
convertPriceToInteger,
|
|
convertSharesToBigInt,
|
|
STOCK_CLASS_ID,
|
|
} from '@/lib/utils/round';
|
|
import { RoundCreateData } from '@/lib/types/company';
|
|
import { PREDEFINED_WALLETS } from '@/lib/constants/wallets';
|
|
|
|
export interface InvestorFormData {
|
|
name: string;
|
|
address: string;
|
|
shares: string;
|
|
pricePerShare: string;
|
|
}
|
|
|
|
export interface CreateRoundFormData {
|
|
roundName: string;
|
|
roundDate: string;
|
|
investors: InvestorFormData[];
|
|
preMoneyValuationForm: string;
|
|
}
|
|
|
|
interface EncryptedInvestorData {
|
|
address: `0x${string}`;
|
|
name: string;
|
|
shares: number;
|
|
pricePerShare: number;
|
|
encryptedShares: `0x${string}`;
|
|
encryptedPrice: `0x${string}`;
|
|
encryptedPreMoneyValuation: `0x${string}`;
|
|
id: `0x${string}`;
|
|
}
|
|
|
|
type CreateRoundStep =
|
|
| 'idle'
|
|
| 'encrypting'
|
|
| 'contract'
|
|
| 'saving'
|
|
| 'complete';
|
|
|
|
export function useCreateRound(companyId: string, contractAddress: string) {
|
|
const { address, proxyAccount } = useAccount();
|
|
const queryClient = useQueryClient();
|
|
const { data: fhevmInstance, isLoading: isFhevmLoading } = useFhevm();
|
|
|
|
const [step, setStep] = useState<CreateRoundStep>('idle');
|
|
const [encryptedData, setEncryptedData] = useState<EncryptedInvestorData[]>(
|
|
[]
|
|
);
|
|
const [roundId, setRoundId] = useState<string>('');
|
|
const [preMoneyValuation, setPreMoneyValuation] = useState<number>(0);
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
const [inputProof, setInputProof] = useState<string>('');
|
|
|
|
// Smart contract interaction
|
|
const {
|
|
writeContract,
|
|
data: hash,
|
|
isPending,
|
|
error: contractError,
|
|
} = useWriteContract();
|
|
|
|
// Wait for transaction confirmation
|
|
const {
|
|
data: receipt,
|
|
isLoading: isConfirming,
|
|
isSuccess: isConfirmed,
|
|
} = useWaitForTransactionReceipt({
|
|
hash,
|
|
});
|
|
|
|
// Backend API call mutation
|
|
const saveRoundMutation = useMutation({
|
|
mutationFn: async (roundData: RoundCreateData): Promise<void> => {
|
|
const response = await fetch(`/api/companies/${companyId}/rounds`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify(roundData),
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (!response.ok) {
|
|
throw new Error(result.error || 'Failed to create round');
|
|
}
|
|
},
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['company', companyId] });
|
|
queryClient.invalidateQueries({ queryKey: ['companies'] });
|
|
setStep('complete');
|
|
},
|
|
});
|
|
|
|
// Watch for contract confirmation and proceed to saving
|
|
useEffect(() => {
|
|
if (isConfirmed && step === 'contract') {
|
|
setStep('saving');
|
|
}
|
|
}, [isConfirmed, step]);
|
|
|
|
const encryptInvestorData = async (
|
|
investors: InvestorFormData[],
|
|
preMoneyValuationForm: string
|
|
): Promise<{
|
|
encryptedInvestors: EncryptedInvestorData[];
|
|
proof: string;
|
|
}> => {
|
|
if (!fhevmInstance || !address) {
|
|
throw new Error('FHEVM instance or wallet not available');
|
|
}
|
|
|
|
// Create encrypted input buffer
|
|
const buffer = fhevmInstance.createEncryptedInput(
|
|
contractAddress as `0x${string}`,
|
|
address
|
|
);
|
|
|
|
const encryptedInvestors: EncryptedInvestorData[] = [];
|
|
|
|
// Add all investor data to the buffer
|
|
for (const investor of investors) {
|
|
const shares = parseFloat(investor.shares);
|
|
const pricePerShare = parseFloat(investor.pricePerShare);
|
|
const preMoneyValuation = parseFloat(preMoneyValuationForm);
|
|
|
|
if (shares <= 0 || pricePerShare <= 0 || preMoneyValuation <= 0) {
|
|
throw new Error(
|
|
`Invalid shares or price for investor ${investor.name}`
|
|
);
|
|
}
|
|
|
|
const sharesBigInt = convertSharesToBigInt(shares);
|
|
const priceBigInt = convertPriceToInteger(pricePerShare);
|
|
const preMoneyValuationBigInt = convertPriceToInteger(preMoneyValuation);
|
|
|
|
// Add encrypted values to buffer
|
|
buffer.add64(sharesBigInt);
|
|
buffer.add64(priceBigInt);
|
|
buffer.add64(preMoneyValuationBigInt);
|
|
|
|
encryptedInvestors.push({
|
|
address: investor.address as `0x${string}`,
|
|
name: investor.name,
|
|
shares,
|
|
pricePerShare,
|
|
encryptedShares: '0x', // Will be filled after encryption
|
|
encryptedPrice: '0x', // Will be filled after encryption
|
|
encryptedPreMoneyValuation: '0x', // Will be filled after encryption
|
|
id: generateBytes16Id(),
|
|
});
|
|
}
|
|
|
|
// Encrypt all values
|
|
const ciphertexts = await buffer.encrypt();
|
|
|
|
// Map encrypted handles back to investors (3 handles per investor: shares + price + pre-money valuation)
|
|
for (let i = 0; i < encryptedInvestors.length; i++) {
|
|
const sharesIndex = i * 3;
|
|
const priceIndex = i * 3 + 1;
|
|
const preMoneyValuationIndex = i * 3 + 2;
|
|
|
|
// Convert Uint8Array handles to hex strings
|
|
encryptedInvestors[i].encryptedShares =
|
|
`0x${Buffer.from(ciphertexts.handles[sharesIndex]).toString('hex')}`;
|
|
encryptedInvestors[i].encryptedPrice =
|
|
`0x${Buffer.from(ciphertexts.handles[priceIndex]).toString('hex')}`;
|
|
encryptedInvestors[i].encryptedPreMoneyValuation =
|
|
`0x${Buffer.from(ciphertexts.handles[preMoneyValuationIndex]).toString('hex')}`;
|
|
}
|
|
|
|
return {
|
|
encryptedInvestors,
|
|
proof: `0x${Buffer.from(ciphertexts.inputProof).toString('hex')}`,
|
|
};
|
|
};
|
|
|
|
const createRound = async (formData: CreateRoundFormData) => {
|
|
if (!address) {
|
|
throw new Error('Wallet not connected');
|
|
}
|
|
|
|
if (!fhevmInstance) {
|
|
throw new Error('FHEVM not initialized');
|
|
}
|
|
|
|
try {
|
|
// Generate round ID and store pre-money valuation
|
|
const generatedRoundId = generateRoundId();
|
|
const preMoneyVal = parseFloat(formData.preMoneyValuationForm);
|
|
|
|
setRoundId(generatedRoundId);
|
|
setPreMoneyValuation(preMoneyVal);
|
|
|
|
// Step 1: Encrypt investor data
|
|
setStep('encrypting');
|
|
const { encryptedInvestors, proof } = await encryptInvestorData(
|
|
formData.investors,
|
|
formData.preMoneyValuationForm
|
|
);
|
|
|
|
setEncryptedData(encryptedInvestors);
|
|
setInputProof(proof);
|
|
|
|
// Step 2: Prepare contract parameters
|
|
const issueParams = encryptedInvestors.map(investor => ({
|
|
id: investor.id,
|
|
stock_class_id: STOCK_CLASS_ID,
|
|
share_price: investor.encryptedPrice,
|
|
quantity: investor.encryptedShares,
|
|
stakeholder_address: investor.address,
|
|
security_id: investor.id,
|
|
custom_id: '',
|
|
stock_legend_ids_mapping: '',
|
|
security_law_exemptions_mapping: '',
|
|
admin_viewer: PREDEFINED_WALLETS[5].address as `0x${string}`,
|
|
round_id: generatedRoundId as `0x${string}`,
|
|
pre_money_valuation: investor.encryptedPreMoneyValuation,
|
|
}));
|
|
|
|
// Step 3: Call smart contract
|
|
setStep('contract');
|
|
writeContract({
|
|
account: proxyAccount,
|
|
address: contractAddress as `0x${string}`,
|
|
abi: privateStockFacetAbi,
|
|
functionName: 'issuePrivateStocks',
|
|
args: [issueParams, proof as `0x${string}`],
|
|
});
|
|
} catch (error) {
|
|
setStep('idle');
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
const saveToDatabase = async (roundName: string, roundDate: string) => {
|
|
if (step !== 'saving') {
|
|
throw new Error('Cannot save to database at this step');
|
|
}
|
|
|
|
// Prepare API payload with investments including share amounts and prices
|
|
const apiInvestments = encryptedData.map(investor => ({
|
|
shareAmount: investor.shares,
|
|
sharePrice: investor.pricePerShare,
|
|
investor: {
|
|
address: investor.address,
|
|
name: investor.name,
|
|
securityId: investor.id, // Include the security ID for database storage
|
|
},
|
|
}));
|
|
|
|
await saveRoundMutation.mutateAsync({
|
|
type: roundName,
|
|
date: roundDate,
|
|
round_id: roundId,
|
|
preMoneyValuation: preMoneyValuation,
|
|
investments: apiInvestments,
|
|
});
|
|
};
|
|
|
|
const reset = () => {
|
|
setStep('idle');
|
|
setEncryptedData([]);
|
|
setRoundId('');
|
|
setPreMoneyValuation(0);
|
|
setInputProof('');
|
|
};
|
|
|
|
return {
|
|
createRound,
|
|
saveToDatabase,
|
|
reset,
|
|
step,
|
|
hash,
|
|
receipt,
|
|
isPending,
|
|
isConfirming,
|
|
isConfirmed,
|
|
isSaving: saveRoundMutation.isPending,
|
|
isEncrypting: step === 'encrypting',
|
|
isFhevmLoading,
|
|
error: contractError || saveRoundMutation.error,
|
|
canProceed: {
|
|
encrypt: !isFhevmLoading && !!fhevmInstance && !!address,
|
|
contract: step === 'contract' && !isPending,
|
|
save: step === 'saving' && !saveRoundMutation.isPending,
|
|
},
|
|
};
|
|
}
|