Skip to main content

cofhejs Integration

Guide to using cofhejs for client-side FHE operations with Lunarys Protocol.

Overview

cofhejs is the JavaScript/TypeScript library for interacting with Fhenix CoFHE. It provides:

  • Client-side encryption of values
  • Decryption of permitted values
  • Type-safe encrypted value handling
  • Integration with ethers.js and viem

Installation

npm install cofhejs

Basic Setup

Create Client

import { createCofheClient } from 'cofhejs'

const cofhe = await createCofheClient({
rpcUrl: 'https://ethereum-sepolia-rpc.publicnode.com',
})

With Provider

import { createCofheClient } from 'cofhejs'
import { BrowserProvider } from 'ethers'

const provider = new BrowserProvider(window.ethereum)
const signer = await provider.getSigner()

const cofhe = await createCofheClient({
provider,
signer,
})

Encryption

Encrypt Values

import { FheType } from 'cofhejs'

// Encrypt different types
const uint8Value = await cofhe.encrypt(255, FheType.Uint8)
const uint16Value = await cofhe.encrypt(65535, FheType.Uint16)
const uint32Value = await cofhe.encrypt(4294967295n, FheType.Uint32)
const uint64Value = await cofhe.encrypt(BigInt(10) ** BigInt(18), FheType.Uint64)
const uint128Value = await cofhe.encrypt(BigInt(10) ** BigInt(30), FheType.Uint128)

FHE Types

TypeRangeUse Case
Uint80 - 255Small values, flags
Uint160 - 65,535Medium values
Uint320 - 4.29BFees, timestamps
Uint640 - 18.4 quintillionToken amounts
Uint1280 - 3.4e38Large amounts, accumulators
Booltrue/falseConditional logic

Encryption Result

The encrypt function returns an InEuint* struct suitable for contract calls:

interface InEuint64 {
data: Uint8Array // Encrypted ciphertext
}

Working with Contracts

Swap Example

import { FheType } from 'cofhejs'
import { parseEther } from 'viem'

async function executeSwap(
poolContract: Contract,
amount: string,
recipient: string
) {
// Convert to wei
const amountWei = parseEther(amount)

// Encrypt for the pool
const encryptedAmount = await cofhe.encrypt(
amountWei,
FheType.Uint64
)

// Execute swap
const tx = await poolContract.swapExactAForB(
encryptedAmount,
recipient,
'0x' // hook context
)

return tx
}

Add Liquidity Example

async function addLiquidity(
poolContract: Contract,
amountA: string,
amountB: string
) {
const encAmountA = await cofhe.encrypt(
parseEther(amountA),
FheType.Uint64
)

const encAmountB = await cofhe.encrypt(
parseEther(amountB),
FheType.Uint64
)

return poolContract.contributeLiquidity(encAmountA, encAmountB)
}

Decryption

Request Decryption

Decryption is asynchronous and requires:

  1. Permission to decrypt
  2. A decryption request
  3. Waiting for the result
// Request decryption through contract
await tokenContract.requestBalanceDecryption()

// Poll for result
async function pollDecryption(tokenContract, address) {
while (true) {
const [balance, ready] = await tokenContract.getDecryptedBalance(address)
if (ready) {
return balance
}
await sleep(2000) // Wait 2 seconds
}
}

Using Decryption Results

async function getDecryptedBalance(
tokenContract: Contract,
address: string
): Promise<bigint | null> {
try {
// Request decryption
await tokenContract.requestBalanceDecryption()

// Wait for result (with timeout)
const timeout = 30000 // 30 seconds
const start = Date.now()

while (Date.now() - start < timeout) {
const [balance, ready] = await tokenContract.getDecryptedBalance(address)
if (ready) {
return balance
}
await new Promise(r => setTimeout(r, 2000))
}

throw new Error('Decryption timeout')
} catch (error) {
console.error('Decryption failed:', error)
return null
}
}

Type Conversion

BigInt to Encrypted

// From number
const fromNumber = await cofhe.encrypt(1000, FheType.Uint64)

// From BigInt
const fromBigInt = await cofhe.encrypt(BigInt('1000000000000000000'), FheType.Uint64)

// From string (via parseEther)
const fromString = await cofhe.encrypt(
parseEther('1.5'),
FheType.Uint64
)

Handling Large Values

// For amounts > Number.MAX_SAFE_INTEGER, use BigInt
const largeAmount = BigInt('100000000000000000000000') // 100,000 tokens

// Encrypt as Uint64 for token amounts
const encrypted = await cofhe.encrypt(largeAmount, FheType.Uint64)

Error Handling

Common Errors

async function safeEncrypt(value: bigint, type: FheType) {
try {
return await cofhe.encrypt(value, type)
} catch (error) {
if (error.message.includes('overflow')) {
throw new Error(`Value ${value} is too large for ${type}`)
}
if (error.message.includes('negative')) {
throw new Error('FHE types are unsigned, negative values not allowed')
}
throw error
}
}

Validation

function validateForEncryption(value: bigint, type: FheType): boolean {
const maxValues: Record<FheType, bigint> = {
[FheType.Uint8]: BigInt(255),
[FheType.Uint16]: BigInt(65535),
[FheType.Uint32]: BigInt(4294967295),
[FheType.Uint64]: BigInt('18446744073709551615'),
[FheType.Uint128]: BigInt('340282366920938463463374607431768211455'),
[FheType.Bool]: BigInt(1),
}

if (value < 0n) return false
if (value > maxValues[type]) return false
return true
}

React Hooks

useEncrypt Hook

import { useState, useCallback } from 'react'
import { FheType } from 'cofhejs'

function useEncrypt(cofheClient) {
const [isEncrypting, setIsEncrypting] = useState(false)
const [error, setError] = useState<Error | null>(null)

const encrypt = useCallback(async (value: bigint, type: FheType) => {
setIsEncrypting(true)
setError(null)
try {
const result = await cofheClient.encrypt(value, type)
return result
} catch (e) {
setError(e as Error)
throw e
} finally {
setIsEncrypting(false)
}
}, [cofheClient])

return { encrypt, isEncrypting, error }
}

useCofhe Provider

import { createContext, useContext, useState, useEffect } from 'react'
import { createCofheClient } from 'cofhejs'

const CofheContext = createContext(null)

export function CofheProvider({ children, rpcUrl }) {
const [client, setClient] = useState(null)

useEffect(() => {
createCofheClient({ rpcUrl }).then(setClient)
}, [rpcUrl])

return (
<CofheContext.Provider value={client}>
{children}
</CofheContext.Provider>
)
}

export function useCofhe() {
return useContext(CofheContext)
}

Best Practices

1. Reuse Client Instance

// Create once at app initialization
const cofhe = await createCofheClient({ rpcUrl })

// Reuse throughout the application
export { cofhe }

2. Pre-encrypt When Possible

// Encrypt before user clicks submit
const [pendingEncryption, setPendingEncryption] = useState(null)

useEffect(() => {
if (inputAmount && !isNaN(parseFloat(inputAmount))) {
cofhe.encrypt(parseEther(inputAmount), FheType.Uint64)
.then(setPendingEncryption)
.catch(console.error)
}
}, [inputAmount])

3. Handle Network Changes

useEffect(() => {
// Reinitialize client on network change
const handleChainChanged = async () => {
const newClient = await createCofheClient({
rpcUrl: getRpcUrl(chainId)
})
setClient(newClient)
}

window.ethereum?.on('chainChanged', handleChainChanged)
return () => {
window.ethereum?.removeListener('chainChanged', handleChainChanged)
}
}, [chainId])

4. Cache Decryption Results

const decryptionCache = new Map<string, bigint>()

async function getDecryptedValue(handle: string): Promise<bigint> {
if (decryptionCache.has(handle)) {
return decryptionCache.get(handle)!
}

const value = await requestAndWaitForDecryption(handle)
decryptionCache.set(handle, value)
return value
}