Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ FHEVM (Fully Homomorphic Encryption Virtual Machine) enables computation on encr
- **🔗 RainbowKit**: Seamless wallet connection and management
- **🌐 Multi-Network Support**: Works on both Sepolia testnet and local Hardhat node
- **📦 Monorepo Structure**: Organized packages for SDK, contracts, and frontend
- **🧩 Vue Composables**: Composition API utilities available via `@fhevm-sdk/vue`

## 📋 Prerequinextjss

Expand Down
16 changes: 15 additions & 1 deletion packages/fhevm-sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@
"./react": {
"types": "./src/react/index.ts",
"default": "./dist/react/index.js"
},
"./vue": {
"types": "./src/vue/index.ts",
"default": "./dist/vue/index.js"
}
},
"scripts": {
Expand All @@ -42,7 +46,16 @@
"@fhevm/mock-utils": "^0.1.0",
"@zama-fhe/relayer-sdk": "^0.2.0",
"ethers": "^6.13.4",
"react": "^18.0.0 || ^19.0.0"
"react": "^18.0.0 || ^19.0.0",
"vue": "^3.4.0"
},
"peerDependenciesMeta": {
"react": {
"optional": true
},
"vue": {
"optional": true
}
},
"devDependencies": {
"@types/node": "~18.19.50",
Expand All @@ -53,6 +66,7 @@
"fake-indexeddb": "~6.0.0",
"jsdom": "^27.0.0",
"react": "~19.0.0",
"vue": "^3.4.0",
"typescript": "~5.8.2",
"vitest": "~2.1.8"
}
Expand Down
64 changes: 64 additions & 0 deletions packages/fhevm-sdk/src/internal/encryptionUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
export type EncryptResult = {
handles: Uint8Array[];
inputProof: Uint8Array;
};

// Map external encrypted integer type to RelayerEncryptedInput builder method
export const getEncryptionMethod = (internalType: string) => {
switch (internalType) {
case "externalEbool":
return "addBool" as const;
case "externalEuint8":
return "add8" as const;
case "externalEuint16":
return "add16" as const;
case "externalEuint32":
return "add32" as const;
case "externalEuint64":
return "add64" as const;
case "externalEuint128":
return "add128" as const;
case "externalEuint256":
return "add256" as const;
case "externalEaddress":
return "addAddress" as const;
default:
console.warn(`Unknown internalType: ${internalType}, defaulting to add64`);
return "add64" as const;
}
};

// Convert Uint8Array or hex-like string to 0x-prefixed hex string
export const toHex = (value: Uint8Array | string): `0x${string}` => {
if (typeof value === "string") {
return (value.startsWith("0x") ? value : `0x${value}`) as `0x${string}`;
}
// value is Uint8Array
return ("0x" + Buffer.from(value).toString("hex")) as `0x${string}`;
};

// Build contract params from EncryptResult and ABI for a given function
export const buildParamsFromAbi = (enc: EncryptResult, abi: any[], functionName: string): any[] => {
const fn = abi.find((item: any) => item.type === "function" && item.name === functionName);
if (!fn) throw new Error(`Function ABI not found for ${functionName}`);

return fn.inputs.map((input: any, index: number) => {
const raw = index === 0 ? enc.handles[0] : enc.inputProof;
switch (input.type) {
case "bytes32":
case "bytes":
return toHex(raw);
case "uint256":
return BigInt(raw as unknown as string);
case "address":
case "string":
return raw as unknown as string;
case "bool":
return Boolean(raw);
default:
console.warn(`Unknown ABI param type ${input.type}; passing as hex`);
return toHex(raw);
}
});
};

68 changes: 4 additions & 64 deletions packages/fhevm-sdk/src/react/useFHEEncryption.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,74 +4,14 @@ import { useCallback, useMemo } from "react";
import { FhevmInstance } from "../fhevmTypes.js";
import { RelayerEncryptedInput } from "@zama-fhe/relayer-sdk/web";
import { ethers } from "ethers";
import type { EncryptResult } from "../internal/encryptionUtils.js";

export type EncryptResult = {
handles: Uint8Array[];
inputProof: Uint8Array;
};

// Map external encrypted integer type to RelayerEncryptedInput builder method
export const getEncryptionMethod = (internalType: string) => {
switch (internalType) {
case "externalEbool":
return "addBool" as const;
case "externalEuint8":
return "add8" as const;
case "externalEuint16":
return "add16" as const;
case "externalEuint32":
return "add32" as const;
case "externalEuint64":
return "add64" as const;
case "externalEuint128":
return "add128" as const;
case "externalEuint256":
return "add256" as const;
case "externalEaddress":
return "addAddress" as const;
default:
console.warn(`Unknown internalType: ${internalType}, defaulting to add64`);
return "add64" as const;
}
};

// Convert Uint8Array or hex-like string to 0x-prefixed hex string
export const toHex = (value: Uint8Array | string): `0x${string}` => {
if (typeof value === "string") {
return (value.startsWith("0x") ? value : `0x${value}`) as `0x${string}`;
}
// value is Uint8Array
return ("0x" + Buffer.from(value).toString("hex")) as `0x${string}`;
};

// Build contract params from EncryptResult and ABI for a given function
export const buildParamsFromAbi = (enc: EncryptResult, abi: any[], functionName: string): any[] => {
const fn = abi.find((item: any) => item.type === "function" && item.name === functionName);
if (!fn) throw new Error(`Function ABI not found for ${functionName}`);

return fn.inputs.map((input: any, index: number) => {
const raw = index === 0 ? enc.handles[0] : enc.inputProof;
switch (input.type) {
case "bytes32":
case "bytes":
return toHex(raw);
case "uint256":
return BigInt(raw as unknown as string);
case "address":
case "string":
return raw as unknown as string;
case "bool":
return Boolean(raw);
default:
console.warn(`Unknown ABI param type ${input.type}; passing as hex`);
return toHex(raw);
}
});
};
export { buildParamsFromAbi, getEncryptionMethod, toHex } from "../internal/encryptionUtils.js";
export type { EncryptResult } from "../internal/encryptionUtils.js";

export const useFHEEncryption = (params: {
instance: FhevmInstance | undefined;
ethersSigner: ethers.JsonRpcSigner | undefined;
ethersSigner: ethers.Signer | undefined;
contractAddress: `0x${string}` | undefined;
}) => {
const { instance, ethersSigner, contractAddress } = params;
Expand Down
4 changes: 4 additions & 0 deletions packages/fhevm-sdk/src/vue/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from "./useFhevm";
export * from "./useFHEEncryption";
export * from "./useFHEDecrypt";
export * from "./useInMemoryStorage";
156 changes: 156 additions & 0 deletions packages/fhevm-sdk/src/vue/useFHEDecrypt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import { computed, ref, shallowRef, unref } from "vue";
import type { Ref } from "vue";
import { ethers } from "ethers";
import { FhevmDecryptionSignature } from "../FhevmDecryptionSignature.js";
import { GenericStringStorage } from "../storage/GenericStringStorage.js";
import type { FhevmInstance } from "../fhevmTypes.js";

export type FHEDecryptRequest = { handle: string; contractAddress: `0x${string}` };

type MaybeRef<T> = T | Ref<T>;

type DecryptResult = Record<string, string | bigint | boolean>;

export const useFHEDecrypt = (params: {
instance: MaybeRef<FhevmInstance | undefined>;
ethersSigner: MaybeRef<ethers.JsonRpcSigner | undefined>;
fhevmDecryptionSignatureStorage: MaybeRef<GenericStringStorage>;
chainId: MaybeRef<number | undefined>;
requests: MaybeRef<readonly FHEDecryptRequest[] | undefined>;
}) => {
const instanceRef = computed(() => unref(params.instance));
const signerRef = computed(() => unref(params.ethersSigner));
const storageRef = computed(() => unref(params.fhevmDecryptionSignatureStorage));
const chainIdRef = computed(() => unref(params.chainId));
const requestsRef = computed(() => unref(params.requests));

const isDecrypting = ref(false);
const message = ref("");
const results = shallowRef<DecryptResult>({});
const error = ref<string | null>(null);

let isDecryptingGuard = false;
const lastRequestsKey = ref("");

const requestsKey = computed(() => {
const requests = requestsRef.value;
if (!requests || requests.length === 0) return "";

const sorted = [...requests].sort((a, b) =>
(a.handle + a.contractAddress).localeCompare(b.handle + b.contractAddress),
);
return JSON.stringify(sorted);
});

const canDecrypt = computed(() => {
const instance = instanceRef.value;
const signer = signerRef.value;
const requests = requestsRef.value;
return Boolean(instance && signer && requests && requests.length > 0 && !isDecrypting.value);
});

const decrypt = async () => {
if (isDecryptingGuard) return;

const instance = instanceRef.value;
const signer = signerRef.value;
const requests = requestsRef.value;
const storage = storageRef.value;
const chainId = chainIdRef.value;

if (!instance || !signer || !requests || requests.length === 0) return;

lastRequestsKey.value = requestsKey.value;

isDecryptingGuard = true;
isDecrypting.value = true;
message.value = "Start decrypt";
error.value = null;

const isStale = () =>
chainId !== chainIdRef.value || signer !== signerRef.value || requestsKey.value !== lastRequestsKey.value;

try {
const uniqueAddresses = Array.from(new Set(requests.map((r: FHEDecryptRequest) => r.contractAddress)));
const sig = await FhevmDecryptionSignature.loadOrSign(
instance,
uniqueAddresses as `0x${string}`[],
signer,
storage,
);

if (!sig) {
message.value = "Unable to build FHEVM decryption signature";
error.value = "SIGNATURE_ERROR: Failed to create decryption signature";
return;
}

if (isStale()) {
message.value = "Ignore FHEVM decryption";
return;
}

message.value = "Call FHEVM userDecrypt...";

const mutableReqs = requests.map((r: FHEDecryptRequest) => ({ handle: r.handle, contractAddress: r.contractAddress }));
let decryptResult: DecryptResult = {};
try {
decryptResult = await instance.userDecrypt(
mutableReqs,
sig.privateKey,
sig.publicKey,
sig.signature,
sig.contractAddresses,
sig.userAddress,
sig.startTimestamp,
sig.durationDays,
);
} catch (e) {
const err = e as { name?: string; message?: string } | undefined;
const code = err && err.name ? err.name : "DECRYPT_ERROR";
const msg = err && err.message ? err.message : "Decryption failed";
error.value = `${code}: ${msg}`;
message.value = "FHEVM userDecrypt failed";
return;
}

message.value = "FHEVM userDecrypt completed!";

if (isStale()) {
message.value = "Ignore FHEVM decryption";
return;
}

results.value = decryptResult;
} catch (e) {
const err = e as { name?: string; message?: string } | undefined;
const code = err && err.name ? err.name : "UNKNOWN_ERROR";
const msg = err && err.message ? err.message : "Unknown error";
error.value = `${code}: ${msg}`;
message.value = "FHEVM decryption errored";
} finally {
isDecryptingGuard = false;
isDecrypting.value = false;
lastRequestsKey.value = requestsKey.value;
}
};

const setMessage = (value: string) => {
message.value = value;
};

const setError = (value: string | null) => {
error.value = value;
};

return {
canDecrypt,
decrypt,
isDecrypting,
message,
results,
error,
setMessage,
setError,
} as const;
};
47 changes: 47 additions & 0 deletions packages/fhevm-sdk/src/vue/useFHEEncryption.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { computed, unref } from "vue";
import type { Ref } from "vue";
import type { Signer } from "ethers";
import type { FhevmInstance } from "../fhevmTypes.js";
import type { EncryptResult } from "../internal/encryptionUtils.js";

export { buildParamsFromAbi, getEncryptionMethod, toHex } from "../internal/encryptionUtils.js";
export type { EncryptResult } from "../internal/encryptionUtils.js";

type MaybeRef<T> = T | Ref<T>;

type RelayerEncryptedInputLike = {
encrypt(): Promise<EncryptResult>;
};

export const useFHEEncryption = (params: {
instance: MaybeRef<FhevmInstance | undefined>;
ethersSigner: MaybeRef<Signer | undefined>;
contractAddress: MaybeRef<`0x${string}` | undefined>;
}) => {
const instanceRef = computed(() => unref(params.instance));
const signerRef = computed(() => unref(params.ethersSigner));
const contractAddressRef = computed(() => unref(params.contractAddress));

const canEncrypt = computed(() => Boolean(instanceRef.value && signerRef.value && contractAddressRef.value));

const encryptWith = async (
buildFn: (builder: RelayerEncryptedInputLike) => void,
): Promise<EncryptResult | undefined> => {
const instance = instanceRef.value;
const signer = signerRef.value;
const contractAddress = contractAddressRef.value;

if (!instance || !signer || !contractAddress) return undefined;

const userAddress = await signer.getAddress();
const input = instance.createEncryptedInput(contractAddress, userAddress) as RelayerEncryptedInputLike;
buildFn(input);
const enc = await input.encrypt();
return enc;
};

return {
canEncrypt,
encryptWith,
} as const;
};
Loading