Frontend Integration
Guide for integrating Lunarys Protocol into web applications.
Overview
The Lunarys frontend is built with:
- Next.js 15
- React 19
- TypeScript
- Reown AppKit (wallet connection)
- cofhejs (FHE encryption)
Setup
Install Dependencies
npm install cofhejs viem wagmi @reown/appkit
Configure Wallet Connection
import { createAppKit } from '@reown/appkit/react'
import { WagmiProvider } from 'wagmi'
const projectId = process.env.NEXT_PUBLIC_REOWN_PROJECT_ID
createAppKit({
projectId,
networks: [sepolia, arbitrumSepolia],
// ... additional config
})
Initialize CoFHE Client
import { createCofheClient, FheType } from 'cofhejs'
const cofheClient = await createCofheClient({
rpcUrl: 'https://ethereum-sepolia-rpc.publicnode.com',
})
Encrypting Values
Basic Encryption
// Encrypt a swap amount
const amount = BigInt(10) * BigInt(10 ** 18) // 10 tokens
const encryptedAmount = await cofheClient.encrypt(amount, FheType.Uint64)
// encryptedAmount is now an InEuint64 that can be passed to contracts
With Permission
// Encrypt with permission for specific address
const encryptedAmount = await cofheClient.encrypt(
amount,
FheType.Uint64,
{ allowedAddress: poolAddress }
)
Contract Interaction
Reading Pool State
import { useReadContract } from 'wagmi'
import { PrivacyPoolABI } from './abis'
function PoolInfo({ poolAddress }) {
const { data: initialized } = useReadContract({
address: poolAddress,
abi: PrivacyPoolABI,
functionName: 'reservesInitialized',
})
const { data: handles } = useReadContract({
address: poolAddress,
abi: PrivacyPoolABI,
functionName: 'latestReserveHandles',
})
return (
<div>
<p>Pool initialized: {initialized ? 'Yes' : 'No'}</p>
<p>Reserve A handle: {handles?.[0]}</p>
<p>Reserve B handle: {handles?.[1]}</p>
</div>
)
}
Executing Swaps
import { useWriteContract, useWaitForTransactionReceipt } from 'wagmi'
function SwapForm({ poolAddress, tokenA, tokenB }) {
const [amount, setAmount] = useState('')
const { writeContract, data: hash, isPending } = useWriteContract()
const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({ hash })
async function handleSwap() {
// Encrypt the amount
const amountBigInt = parseEther(amount)
const encryptedAmount = await cofheClient.encrypt(amountBigInt, FheType.Uint64)
// Execute swap
writeContract({
address: poolAddress,
abi: PrivacyPoolABI,
functionName: 'swapExactAForB',
args: [encryptedAmount, address, '0x'],
})
}
return (
<form onSubmit={handleSwap}>
<input
type="text"
value={amount}
onChange={(e) => setAmount(e.target.value)}
placeholder="Amount"
/>
<button type="submit" disabled={isPending || isConfirming}>
{isPending ? 'Confirming...' : isConfirming ? 'Processing...' : 'Swap'}
</button>
{isSuccess && <p>Swap successful!</p>}
</form>
)
}
Adding Liquidity
async function addLiquidity(amountA: string, amountB: string) {
// Encrypt both amounts
const encAmountA = await cofheClient.encrypt(
parseEther(amountA),
FheType.Uint64
)
const encAmountB = await cofheClient.encrypt(
parseEther(amountB),
FheType.Uint64
)
writeContract({
address: poolAddress,
abi: PrivacyPoolABI,
functionName: 'contributeLiquidity',
args: [encAmountA, encAmountB],
})
}
Wrapping Tokens
async function wrapTokens(amount: string) {
writeContract({
address: tokenAddress,
abi: TestFHERC20ABI,
functionName: 'wrap',
args: [parseEther(amount)],
})
}
Decrypting Values
Request Decryption
async function requestBalanceDecryption() {
writeContract({
address: tokenAddress,
abi: TestFHERC20ABI,
functionName: 'requestBalanceDecryption',
})
}
Read Decrypted Value
function DecryptedBalance({ tokenAddress, userAddress }) {
const { data } = useReadContract({
address: tokenAddress,
abi: TestFHERC20ABI,
functionName: 'getDecryptedBalance',
args: [userAddress],
})
const [balance, isReady] = data || [0n, false]
if (!isReady) {
return <p>Decryption pending...</p>
}
return <p>Balance: {formatEther(balance)}</p>
}
Position Management
Fetching User Positions
function UserPositions({ positionNFTAddress, userAddress }) {
const { data: positionIds } = useReadContract({
address: positionNFTAddress,
abi: PositionNFTABI,
functionName: 'getUserPositions',
args: [userAddress],
})
return (
<div>
{positionIds?.map((id) => (
<PositionCard key={id} tokenId={id} />
))}
</div>
)
}
Position Details
function PositionCard({ tokenId, positionNFTAddress }) {
const { data: position } = useReadContract({
address: positionNFTAddress,
abi: PositionNFTABI,
functionName: 'getPosition',
args: [tokenId],
})
return (
<div>
<p>Position #{tokenId.toString()}</p>
<p>Token0: {position?.token0}</p>
<p>Token1: {position?.token1}</p>
<p>Created: {new Date(Number(position?.createdAt) * 1000).toLocaleDateString()}</p>
</div>
)
}
Error Handling
Transaction Errors
function SwapButton() {
const { writeContract, error } = useWriteContract()
useEffect(() => {
if (error) {
if (error.message.includes('user rejected')) {
toast.error('Transaction rejected by user')
} else if (error.message.includes('insufficient')) {
toast.error('Insufficient balance')
} else {
toast.error('Transaction failed')
}
}
}, [error])
// ...
}
FHE Encryption Errors
async function safeEncrypt(amount: bigint) {
try {
return await cofheClient.encrypt(amount, FheType.Uint64)
} catch (error) {
if (error.message.includes('overflow')) {
throw new Error('Amount too large to encrypt')
}
throw error
}
}
Best Practices
1. Pre-compute Encrypted Values
// Cache encrypted values to avoid re-encryption
const [cachedEncryption, setCachedEncryption] = useState(null)
useEffect(() => {
if (amount) {
cofheClient.encrypt(parseEther(amount), FheType.Uint64)
.then(setCachedEncryption)
}
}, [amount])
2. Handle Gas Estimation
// FHE operations require higher gas limits
writeContract({
// ...
gas: 1_000_000n, // Set explicit gas limit
})
3. Show Pending States
function TransactionStatus({ hash }) {
const { isLoading, isSuccess, isError } = useWaitForTransactionReceipt({ hash })
if (isLoading) return <Spinner />
if (isSuccess) return <CheckIcon />
if (isError) return <ErrorIcon />
return null
}
4. Validate Inputs
function validateSwapAmount(amount: string): boolean {
const parsed = parseFloat(amount)
if (isNaN(parsed) || parsed <= 0) return false
if (parsed > Number.MAX_SAFE_INTEGER) return false
return true
}