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 package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"deploy:sepolia": "pnpm hardhat:deploy:sepolia && pnpm generate",
"fork": "pnpm hardhat:fork",
"format": "pnpm next:format && pnpm hardhat:format",
"sdk": "pnpm --filter fhevm-sdk",
"sdk:build": "pnpm --filter ./packages/fhevm-sdk build",
"sdk:watch": "pnpm --filter ./packages/fhevm-sdk watch",
"sdk:test": "pnpm --filter ./packages/fhevm-sdk test",
Expand Down
48 changes: 48 additions & 0 deletions packages/fhevm-sdk2/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
{
"name": "fhevm-sdk",
"version": "0.1.0",
"private": true,
"type": "module",
"main": "dist/index.js",
"module": "dist/index.js",
"types": "src/index.ts",
"exports": {
".": {
"types": "./src/index.ts",
"default": "./dist/index.js"
},
"./react": {
"types": "./src/react/index.ts",
"default": "./dist/react/index.js"
}
},
"scripts": {
"build": "tsc -p tsconfig.json",
"watch": "tsc -p tsconfig.json --watch",
"clean": "rm -rf dist",
"test": "vitest run --coverage",
"test:watch": "vitest"
},
"dependencies": {
"idb": "^8.0.3",
"loadjs": "^4.3.0"
},
"peerDependencies": {
"@zama-fhe/relayer-sdk": "^0.2.0",
"react": "^18.0.0 || ^19.0.0",
"viem": "2.34.0"
},
"devDependencies": {
"@types/loadjs": "^4.0.4",
"@types/node": "~18.19.50",
"@types/react": "~19.0.7",
"@vitest/coverage-v8": "2.1.9",
"@zama-fhe/relayer-sdk": "0.2.0",
"fake-indexeddb": "~6.0.0",
"jsdom": "^27.0.0",
"react": "~19.0.0",
"typescript": "~5.8.2",
"viem": "2.34.0",
"vitest": "~2.1.8"
}
}
126 changes: 126 additions & 0 deletions packages/fhevm-sdk2/src/core/FhevmClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { type EIP712, type FhevmInstance, type RelayerEncryptedInput } from '@zama-fhe/relayer-sdk/bundle';
import { Address, TypedDataDomain, WalletClient } from 'viem';
import { FhevmEnvironment } from './FhevmEnvironment';

export type FhevmClientInitOptions = {
contractAddress: Address;
durationDays: number;
walletClient: WalletClient;
sdkUrl?: string;
};

export type SignatureCacheItem = {
kp: { publicKey: string; privateKey: string };
contractAddresses: string[];
userAddress: string;
startTimestamp: number;
durationDays: number;
signature: string;
};

export class FhevmClient {
private ins!: FhevmInstance;
private durationDays!: number;
private initOptions!: FhevmClientInitOptions;

async init(options: FhevmClientInitOptions) {
await FhevmEnvironment.init({ sdkUrl: options.sdkUrl });

this.initOptions = options;
this.ins = await (window as any).relayerSDK.createInstance({
...(window as any).relayerSDK.SepoliaConfig,
network: options.walletClient,
});

this.durationDays = options.durationDays;
}

async userDecrypt<T extends bigint | boolean | string>(handle: string) {
this._checkInitStatus();

const { walletClient, contractAddress } = this.initOptions;
const sigItem = await this._loadOrRequestEIP712Signature(contractAddress, walletClient, this.durationDays);

const r = await this.ins!.userDecrypt(
[{ handle, contractAddress }],
sigItem.kp.privateKey,
sigItem.kp.publicKey,
sigItem.signature,
sigItem.contractAddresses,
sigItem.userAddress,
sigItem.startTimestamp,
sigItem.durationDays
);
return r[handle] as T;
}

async userEncrypt(inputFn: (input: RelayerEncryptedInput) => void) {
this._checkInitStatus();

const { walletClient, contractAddress } = this.initOptions;
const encryptedInput = this.ins!.createEncryptedInput(contractAddress, walletClient.account!.address);
// Call the input function to add data to the encrypted input
inputFn(encryptedInput);
return encryptedInput.encrypt();
}

async publicDecrypt(handles: string[]) {
this._checkInitStatus();
return await this.ins!.publicDecrypt(handles);
}

private _checkInitStatus() {
if (!this.ins) {
throw new Error('FhevmClient is not initialized. Call init() first.');
}
}

private async _loadOrRequestEIP712Signature(contractAddress: Address, wc: WalletClient, durationDays: number) {
const userAddress = wc.account!.address;
const cacheKey = `${contractAddress}_${userAddress}`;
let cache = await this._getSignatureCacheItem(cacheKey);
if (!cache) {
const kp = this.ins.generateKeypair();
const ts = Math.floor(Date.now() / 1000);
const eip712 = this.ins.createEIP712(kp.publicKey, [contractAddress], ts, durationDays);
const sig = await this._requestEIP712Signature(wc, eip712);
cache = {
kp,
contractAddresses: [contractAddress],
userAddress: userAddress,
startTimestamp: ts,
durationDays,
signature: sig,
};
await this._saveToCache(cacheKey, cache);
}
return cache;
}

private async _getSignatureCacheItem(cacheKey: string): Promise<SignatureCacheItem | null> {
try {
const value = JSON.parse(localStorage.getItem(cacheKey) as string);
const expiredAt = value.startTimestamp + value.durationDays * 3600 * 24;
if (expiredAt < Math.floor(Date.now() / 1000)) {
return null;
}
return value;
} catch {}
return null;
}

private async _saveToCache(cacheKey: string, item: SignatureCacheItem) {
localStorage.setItem(cacheKey, JSON.stringify(item));
}

private async _requestEIP712Signature(wc: WalletClient, eip712: EIP712) {
const signature = await wc.signTypedData({
account: wc.account!.address,
domain: eip712.domain as TypedDataDomain,
types: eip712.types,
primaryType: eip712.primaryType,
message: eip712.message,
});
return signature;
}
}
69 changes: 69 additions & 0 deletions packages/fhevm-sdk2/src/core/FhevmEnvironment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import loadjs from 'loadjs';
import { SDK_CDN_URL } from './constants';
import { type FhevmInstance } from '@zama-fhe/relayer-sdk/web';
export type FhevmEnvironmentInitOptions = {
/** sdk url, default: https://cdn.zama.ai/relayer-sdk-js/0.2.0/relayer-sdk-js.umd.cjs */
sdkUrl?: string;
};

export type FhevmEnvironmentStatus = 'sdk-loading' | 'sdk-loaded' | 'sdk-initializing' | 'sdk-initialized';

export class FhevmEnvironment {
private static _isFhevmInitialized = false;
private static _fhevmInstance: FhevmInstance;
private static _onStatusChange: (status: FhevmEnvironmentStatus) => void;
static async init(options?: FhevmEnvironmentInitOptions) {
if (this._isFhevmInitialized) {
return true;
}
const sdkUrl = options?.sdkUrl || SDK_CDN_URL;
try {
if (!loadjs.isDefined('relayer-sdk-js')) {
this._notify('sdk-loading');
await loadjs(sdkUrl, 'relayer-sdk-js', { async: true, returnPromise: true });
this._notify('sdk-loaded');
this._notify('sdk-initializing');
await (window as any).relayerSDK.initSDK();
this._notify('sdk-initialized');
}
this._isFhevmInitialized = true;
return true;
} catch (e) {
throw new Error(`Failed to load FHEVM SDK from ${sdkUrl}. Please check the URL or your network connection.`, {
cause: e,
});
}
}

static isFhevmInitialized(): boolean {
return this._isFhevmInitialized;
}

static onStatusChange(handler: (e: FhevmEnvironmentStatus) => void) {
this._onStatusChange = handler;
}

private static _notify(status: FhevmEnvironmentStatus) {
this._onStatusChange?.(status);
}

static async getFhevmInstance(options: { walletClient?: any }) {
if (this._fhevmInstance) {
return this._fhevmInstance;
}
if (!this.isFhevmInitialized()) {
throw new Error('FHEVM SDK is not initialized. Please call FhevmEnvironment.init() first.');
}

if (!options.walletClient) {
throw new Error('Wallet client is required to create an FHEVM instance.');
}

this._fhevmInstance = await (window as any).relayerSDK.createInstance({
...(window as any).relayerSDK.SepoliaConfig,
network: options.walletClient,
});
console.log('FHEVM instance created:', this._fhevmInstance);
return this._fhevmInstance;
}
}
2 changes: 2 additions & 0 deletions packages/fhevm-sdk2/src/core/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const SDK_CDN_URL =
"https://cdn.zama.ai/relayer-sdk-js/0.2.0/relayer-sdk-js.umd.cjs";
5 changes: 5 additions & 0 deletions packages/fhevm-sdk2/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export { FhevmClient, type FhevmClientInitOptions } from './core/FhevmClient';
export { FhevmEnvironment, type FhevmEnvironmentInitOptions } from './core/FhevmEnvironment';

export { useFHEDecryption } from './react/useFHEDecryption';
export { useFHEEncryption } from './react/useFHEEncryption';
26 changes: 26 additions & 0 deletions packages/fhevm-sdk2/src/react/useFHEDecryption.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { useCallback, useMemo } from 'react';
import { FhevmClient } from '../core/FhevmClient';

export type FHEDecryptRequest = { handle: string };

export const useFHEDecryption = (params: {
client: FhevmClient | undefined;
}) => {
const { client } = params;

const canDecrypt = useMemo(() => Boolean(client), [client]);

const decrypt = useCallback(
async (handle: string): Promise<string | bigint | boolean | undefined> => {
if (!client) return undefined;

return await client.userDecrypt(handle);
},
[client],
);

return {
canDecrypt,
decrypt,
} as const;
};
90 changes: 90 additions & 0 deletions packages/fhevm-sdk2/src/react/useFHEEncryption.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { useCallback, useMemo } from 'react';
import { FhevmClient } from '../core/FhevmClient';
import { RelayerEncryptedInput } from '@zama-fhe/relayer-sdk/bundle';

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 const useFHEEncryption = (params: {
client: FhevmClient | undefined;
}) => {
const { client } = params;

const canEncrypt = useMemo(() => Boolean(client), [client]);

const encryptWith = useCallback(
async (buildFn: (builder: RelayerEncryptedInput) => void): Promise<EncryptResult | undefined> => {
if (!client) return undefined;

const enc = await client.userEncrypt(buildFn);
return enc;
},
[client],
);

return {
canEncrypt,
encryptWith,
} as const;
};
13 changes: 13 additions & 0 deletions packages/fhevm-sdk2/test/exports.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { describe, it, expect } from 'vitest';
import * as main from '../src';
import * as react from '../src/react';

describe('exports', () => {
it('main exports are present', () => {
expect(main).toBeTruthy();
});

it('react exports are present', () => {
expect(react).toBeTruthy();
});
});
Loading