Skip to main content

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
}