feat: upgrade and use zerokit, cleanup

This commit is contained in:
Danish Arora 2025-07-24 18:22:11 +05:30
parent 7622d02fe9
commit cd599f0a97
No known key found for this signature in database
GPG Key ID: 1C6EF37CDAE1426E
12 changed files with 1013 additions and 587 deletions

110
package-lock.json generated
View File

@ -19,7 +19,7 @@
"@radix-ui/react-toggle": "^1.1.2",
"@radix-ui/react-toggle-group": "^1.1.2",
"@radix-ui/react-tooltip": "^1.1.8",
"@waku/rln": "0.1.7-987c6cd.0",
"@waku/rln": "0.1.8-e224c05.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"framer-motion": "^12.6.3",
@ -656,6 +656,12 @@
"js-sha3": "0.8.0"
}
},
"node_modules/@ethersproject/keccak256/node_modules/js-sha3": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz",
"integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==",
"license": "MIT"
},
"node_modules/@ethersproject/logger": {
"version": "5.8.0",
"resolved": "https://registry.npmjs.org/@ethersproject/logger/-/logger-5.8.0.tgz",
@ -1307,9 +1313,9 @@
}
},
"node_modules/@libp2p/crypto/node_modules/@noble/curves": {
"version": "1.9.2",
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.2.tgz",
"integrity": "sha512-HxngEd2XUcg9xi20JkwlLCtYwfoFw4JGkuZpT+WlsPD4gB/cxkvTD8fSsoAnphGZhFdZYKeQIPCuFlWPm1uE0g==",
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.4.tgz",
"integrity": "sha512-2bKONnuM53lINoDrSmK8qP8W271ms7pygDhZt4SiLOoLwBtoHqeCFi6RG42V8zd3mLHuJFhU/Bmaqo4nX0/kBw==",
"license": "MIT",
"dependencies": {
"@noble/hashes": "1.8.0"
@ -2977,17 +2983,17 @@
]
},
"node_modules/@waku/core": {
"version": "0.0.37-987c6cd.0",
"resolved": "https://registry.npmjs.org/@waku/core/-/core-0.0.37-987c6cd.0.tgz",
"integrity": "sha512-w3F/dzY/YA5T3l62YKKcza3fMLZNzprupoKScvtpWS8VymLIGZCMhzcKOij4lt6SWzHWyTEMa9qtcB6tZPIC+A==",
"version": "0.0.38-e224c05.0",
"resolved": "https://registry.npmjs.org/@waku/core/-/core-0.0.38-e224c05.0.tgz",
"integrity": "sha512-VtRfwF6WGj3HhB4cywiit3KcrHYtUtoD1wjAviO8b4AzH9LbtV7NmYvGIy5ac9sOpRQEUNagOFT6+oXu7Ay+HQ==",
"license": "MIT OR Apache-2.0",
"dependencies": {
"@libp2p/ping": "2.0.35",
"@noble/hashes": "^1.3.2",
"@waku/enr": "0.0.31-987c6cd.0",
"@waku/interfaces": "0.0.32-987c6cd.0",
"@waku/proto": "0.0.12-987c6cd.0",
"@waku/utils": "0.0.25-987c6cd.0",
"@waku/enr": "0.0.32-e224c05.0",
"@waku/interfaces": "0.0.33-e224c05.0",
"@waku/proto": "0.0.13-e224c05.0",
"@waku/utils": "0.0.26-e224c05.0",
"debug": "^4.3.4",
"it-all": "^3.0.4",
"it-length-prefixed": "^9.0.4",
@ -3025,9 +3031,9 @@
}
},
"node_modules/@waku/enr": {
"version": "0.0.31-987c6cd.0",
"resolved": "https://registry.npmjs.org/@waku/enr/-/enr-0.0.31-987c6cd.0.tgz",
"integrity": "sha512-nStUXohULcatKLxCyzU7JJK2gKhMXYEAdN90uL1Ggl4tKA9uUrQ2YZC/WB61RmucnDlFNH6phFv47/WbARhhVA==",
"version": "0.0.32-e224c05.0",
"resolved": "https://registry.npmjs.org/@waku/enr/-/enr-0.0.32-e224c05.0.tgz",
"integrity": "sha512-f+DEugomxeDgCylxv2xm6eLIJBB76dctB4B/d0zUMld26zjBozRKUsaNn8DOWRT5f44WzyP+5gRqbTm4e/qsMw==",
"license": "MIT OR Apache-2.0",
"dependencies": {
"@ethersproject/rlp": "^5.7.0",
@ -3035,7 +3041,7 @@
"@libp2p/peer-id": "5.1.7",
"@multiformats/multiaddr": "^12.0.0",
"@noble/secp256k1": "^1.7.1",
"@waku/utils": "0.0.25-987c6cd.0",
"@waku/utils": "0.0.26-e224c05.0",
"debug": "^4.3.4",
"js-sha3": "^0.9.2"
},
@ -3079,9 +3085,9 @@
}
},
"node_modules/@waku/enr/node_modules/@noble/curves": {
"version": "1.9.2",
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.2.tgz",
"integrity": "sha512-HxngEd2XUcg9xi20JkwlLCtYwfoFw4JGkuZpT+WlsPD4gB/cxkvTD8fSsoAnphGZhFdZYKeQIPCuFlWPm1uE0g==",
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.4.tgz",
"integrity": "sha512-2bKONnuM53lINoDrSmK8qP8W271ms7pygDhZt4SiLOoLwBtoHqeCFi6RG42V8zd3mLHuJFhU/Bmaqo4nX0/kBw==",
"license": "MIT",
"dependencies": {
"@noble/hashes": "1.8.0"
@ -3105,25 +3111,19 @@
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@waku/enr/node_modules/js-sha3": {
"version": "0.9.3",
"resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.9.3.tgz",
"integrity": "sha512-BcJPCQeLg6WjEx3FE591wVAevlli8lxsxm9/FzV4HXkV49TmBH38Yvrpce6fjbADGMKFrBMGTqrVz3qPIZ88Gg==",
"license": "MIT"
},
"node_modules/@waku/interfaces": {
"version": "0.0.32-987c6cd.0",
"resolved": "https://registry.npmjs.org/@waku/interfaces/-/interfaces-0.0.32-987c6cd.0.tgz",
"integrity": "sha512-7dfGDx1bs+rs1nlMwAZYd0Di4gLyD9cWR0ApDJ+I0sU3enn1NT6hM1U3cp+LE6xqxXUoe2tfQi/4QhrECaGypQ==",
"version": "0.0.33-e224c05.0",
"resolved": "https://registry.npmjs.org/@waku/interfaces/-/interfaces-0.0.33-e224c05.0.tgz",
"integrity": "sha512-8GH7Tg0t74R32NqQHVm0ttVPb26Cvn/p+Iiht3mha8gGHQVkYYYyECXfZYAQ9pld3y0GB+yqpn5EiZiTNs3AOQ==",
"license": "MIT OR Apache-2.0",
"engines": {
"node": ">=22"
}
},
"node_modules/@waku/proto": {
"version": "0.0.12-987c6cd.0",
"resolved": "https://registry.npmjs.org/@waku/proto/-/proto-0.0.12-987c6cd.0.tgz",
"integrity": "sha512-B0u7Qkm/U6mH0ZCGYvRfg4d44JboscYBmXifeOTwY0i4QH8nvrZcPIIsUqvNmzT2CjrAwpt8H+98fGp9l4fKow==",
"version": "0.0.13-e224c05.0",
"resolved": "https://registry.npmjs.org/@waku/proto/-/proto-0.0.13-e224c05.0.tgz",
"integrity": "sha512-zvIGhAECY1v691tWnyiFcUAPvd06itXRq9BggN5Nwq+TKMl9XOL6+kHuTzovvn1PM9ajRyoP3zbqT1musxeixQ==",
"license": "MIT OR Apache-2.0",
"dependencies": {
"protons-runtime": "^5.4.0"
@ -3133,16 +3133,16 @@
}
},
"node_modules/@waku/rln": {
"version": "0.1.7-987c6cd.0",
"resolved": "https://registry.npmjs.org/@waku/rln/-/rln-0.1.7-987c6cd.0.tgz",
"integrity": "sha512-6w/17WpD7jdw5LyM6OX0c2mMxW3cRrXG7wxi7PzhoSb2celNWZYhnPR9UK+rgsP5opoBDXD7LAyaEEqA3afbgA==",
"version": "0.1.8-e224c05.0",
"resolved": "https://registry.npmjs.org/@waku/rln/-/rln-0.1.8-e224c05.0.tgz",
"integrity": "sha512-9uSP1ARwOdpGHJ9LIpNdNo03/q60YYYuDGCX+fPfrDFooWjxvIn0+G1TzxUgJLLP6g7stxaWj+iKCCktNFpxZQ==",
"license": "MIT OR Apache-2.0",
"dependencies": {
"@chainsafe/bls-keystore": "3.0.0",
"@noble/hashes": "^1.2.0",
"@waku/core": "0.0.37-987c6cd.0",
"@waku/utils": "0.0.25-987c6cd.0",
"@waku/zerokit-rln-wasm": "^0.0.13",
"@waku/core": "0.0.38-e224c05.0",
"@waku/utils": "0.0.26-e224c05.0",
"@waku/zerokit-rln-wasm": "^0.2.1",
"chai": "^5.1.2",
"chai-as-promised": "^8.0.1",
"chai-spies": "^1.1.0",
@ -3158,13 +3158,13 @@
}
},
"node_modules/@waku/utils": {
"version": "0.0.25-987c6cd.0",
"resolved": "https://registry.npmjs.org/@waku/utils/-/utils-0.0.25-987c6cd.0.tgz",
"integrity": "sha512-HaYkDnVtpfmsXfDBd0gB63CxidIeE9gKMRMCaaywy6W07HK32ZgMgnD/BoPbaREa4BpEZELvW3qbYUydm3AKzw==",
"version": "0.0.26-e224c05.0",
"resolved": "https://registry.npmjs.org/@waku/utils/-/utils-0.0.26-e224c05.0.tgz",
"integrity": "sha512-eleBm6L5ky5xKRoCMXfFhLmvyGix8dhOXtDIS8/lIr+CJ09cDQoAHhhpPsJnLmf8DRxYQ1PDqPsEet9sa2LT9Q==",
"license": "MIT OR Apache-2.0",
"dependencies": {
"@noble/hashes": "^1.3.2",
"@waku/interfaces": "0.0.32-987c6cd.0",
"@waku/interfaces": "0.0.33-e224c05.0",
"chai": "^4.3.10",
"debug": "^4.3.4",
"uint8arrays": "^5.0.1"
@ -3243,9 +3243,9 @@
}
},
"node_modules/@waku/zerokit-rln-wasm": {
"version": "0.0.13",
"resolved": "https://registry.npmjs.org/@waku/zerokit-rln-wasm/-/zerokit-rln-wasm-0.0.13.tgz",
"integrity": "sha512-x7CRIIslmfCmTZc7yVp3dhLlKeLUs8ILIm9kv7+wVJ23H4pPw0Z+uH0ueLIYYfwODI6fDiwJj3S1vdFzM8D1zA==",
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/@waku/zerokit-rln-wasm/-/zerokit-rln-wasm-0.2.1.tgz",
"integrity": "sha512-2Xp7e92y4qZpsiTPGBSVr4gVJ9mJTLaudlo0DQxNpxJUBtoJKpxdH5xDCQDiorbkWZC2j9EId+ohhxHO/xC1QQ==",
"license": "MIT or Apache2"
},
"node_modules/abort-error": {
@ -3670,9 +3670,9 @@
}
},
"node_modules/bn.js": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz",
"integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==",
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.2.tgz",
"integrity": "sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw==",
"license": "MIT"
},
"node_modules/brace-expansion": {
@ -4392,9 +4392,9 @@
}
},
"node_modules/elliptic/node_modules/bn.js": {
"version": "4.12.1",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz",
"integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==",
"version": "4.12.2",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz",
"integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==",
"license": "MIT"
},
"node_modules/emoji-regex": {
@ -6506,9 +6506,9 @@
}
},
"node_modules/js-sha3": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz",
"integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==",
"version": "0.9.3",
"resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.9.3.tgz",
"integrity": "sha512-BcJPCQeLg6WjEx3FE591wVAevlli8lxsxm9/FzV4HXkV49TmBH38Yvrpce6fjbADGMKFrBMGTqrVz3qPIZ88Gg==",
"license": "MIT"
},
"node_modules/js-tokens": {
@ -8227,9 +8227,9 @@
"license": "MIT"
},
"node_modules/race-event": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/race-event/-/race-event-1.6.0.tgz",
"integrity": "sha512-hXkk3CDepWELBG2MsT/zIiTbjNNucMo49vwZEdjChJlxJivc8fWIu/Gh/4vEJdWsHDmnGCC6++ftP2Afep6RUg==",
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/race-event/-/race-event-1.6.1.tgz",
"integrity": "sha512-vi7WH5g5KoTFpu2mme/HqZiWH14XSOtg5rfp6raBskBHl7wnmy3F/biAIyY5MsK+BHWhoPhxtZ1Y2R7OHHaWyQ==",
"license": "Apache-2.0 OR MIT",
"dependencies": {
"abort-error": "^1.0.1"

View File

@ -21,7 +21,7 @@
"@radix-ui/react-toggle": "^1.1.2",
"@radix-ui/react-toggle-group": "^1.1.2",
"@radix-ui/react-tooltip": "^1.1.8",
"@waku/rln": "0.1.7-987c6cd.0",
"@waku/rln": "0.1.8-e224c05.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"framer-motion": "^12.6.3",

View File

@ -36,21 +36,28 @@ export function MembershipRegistration({ tabId: _tabId }: MembershipRegistration
const isLineaSepolia = chainId === 59141;
const [price, setPrice] = useState<string>('');
// Store prices for both rate limits
const [prices, setPrices] = useState<{ [key: number]: string }>({});
const [priceLoading, setPriceLoading] = useState(false);
const [priceError, setPriceError] = useState<string | null>(null);
useEffect(() => {
if (isLoading || !isInitialized || !isStarted ) return;
if (isLoading || !isInitialized || !isStarted) return;
let cancelled = false;
setPrice('');
setPrices({});
setPriceError(null);
setPriceLoading(true);
(async () => {
try {
const result = await getPriceForRateLimit(rateLimit);
const [price300, price600] = await Promise.all([
getPriceForRateLimit(300),
getPriceForRateLimit(600),
]);
if (!cancelled) {
setPrice(result.price.toString());
setPrices({
300: price300.price.toString(),
600: price600.price.toString(),
});
}
} catch {
if (!cancelled) {
@ -61,7 +68,7 @@ export function MembershipRegistration({ tabId: _tabId }: MembershipRegistration
}
})();
return () => { cancelled = true; };
}, [rateLimit, getPriceForRateLimit, isLoading, isInitialized, isStarted]);
}, [getPriceForRateLimit, isLoading, isInitialized, isStarted]);
useEffect(() => {
if (error) {
@ -234,7 +241,7 @@ export function MembershipRegistration({ tabId: _tabId }: MembershipRegistration
) : priceError ? (
<span className="text-destructive">{priceError}</span>
) : (
<>{rateLimit === 300 && <>Token spend: {price} WTT</>}</>
<>Token spend: {prices[300] ?? "--"} WTT</>
)}
</span>
</ToggleGroupItem>
@ -246,14 +253,12 @@ export function MembershipRegistration({ tabId: _tabId }: MembershipRegistration
) : priceError ? (
<span className="text-destructive">{priceError}</span>
) : (
<>{rateLimit === 600 && <>Token spend: {price} WTT</>}</>
<>Token spend: {prices[600] ?? "--"} WTT</>
)}
</span>
</ToggleGroupItem>
</ToggleGroup>
</div>
{/* Show calculated token spend for selected rate limit */}
{/* Removed redundant price display below, now shown in each ToggleGroupItem */}
</div>
<div className="space-y-2">

View File

@ -1,484 +1,77 @@
"use client";
import { createContext, useContext, useState, useEffect, ReactNode, useCallback } from 'react';
import { KeystoreEntity, MembershipInfo, RLNCredentialsManager } from '@waku/rln';
import { ethers } from 'ethers';
import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
import { RLNInstance } from '@waku/rln';
import { useKeystore } from '../keystore';
import { ERC20_ABI, LINEA_SEPOLIA_CONFIG, ensureLineaSepoliaNetwork } from '../../utils/network';
interface RLNContextType {
rln: RLNCredentialsManager | null;
isInitialized: boolean;
isStarted: boolean;
error: string | null;
initializeRLN: () => Promise<void>;
registerMembership: (rateLimit: number, saveOptions?: { password: string }) => Promise<{
success: boolean;
error?: string;
credentials?: KeystoreEntity;
keystoreHash?: string;
}>;
extendMembership: (hash: string, password: string) => Promise<{ success: boolean; error?: string }>;
eraseMembership: (hash: string, password: string) => Promise<{ success: boolean; error?: string }>;
withdrawDeposit: (hash: string, password: string) => Promise<{ success: boolean; error?: string }>;
getMembershipInfo: (hash: string, password: string) => Promise<MembershipInfo & {
address: string;
chainId: string;
treeIndex: number;
rateLimit: number;
}>;
rateMinLimit: number;
rateMaxLimit: number;
getCurrentRateLimit: () => Promise<number | null>;
getRateLimitsBounds: () => Promise<{ success: boolean; rateMinLimit: number; rateMaxLimit: number; error?: string }>;
saveCredentialsToKeystore: (credentials: KeystoreEntity, password: string) => Promise<string>;
isLoading: boolean;
getPriceForRateLimit: (rateLimit: number) => Promise<{ price: string }>;
}
import { RLNContextType } from './types';
import { useWallet } from './wallet';
import { useRLNInitialization } from './initialization';
import {
getCurrentRateLimit,
getRateLimitsBounds,
getPriceForRateLimit
} from './rateLimits';
import {
registerMembership,
extendMembership,
eraseMembership,
withdrawDeposit,
getMembershipInfo
} from './operations';
const RLNContext = createContext<RLNContextType | undefined>(undefined);
export function RLNProvider({ children }: { children: ReactNode }) {
const [rln, setRln] = useState<RLNCredentialsManager | null>(null);
// State management
const [rln, setRln] = useState<RLNInstance | null>(null);
const [isInitialized, setIsInitialized] = useState(false);
const [isStarted, setIsStarted] = useState(false);
const [error, setError] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
// Get the signer from window.ethereum
const [signer, setSigner] = useState<ethers.Signer | null>(null);
const [isConnected, setIsConnected] = useState(false);
const [rateMinLimit, setRateMinLimit] = useState<number>(0);
const [rateMaxLimit, setRateMaxLimit] = useState<number>(0);
// Hooks
const { signer, isConnected } = useWallet();
const { saveCredentials: saveToKeystore, getDecryptedCredential } = useKeystore();
// Listen for wallet connection
useEffect(() => {
const checkWallet = async () => {
try {
if (window.ethereum) {
const provider = new ethers.providers.Web3Provider(window.ethereum);
const accounts = await provider.listAccounts();
if (accounts.length > 0) {
const signer = provider.getSigner();
setSigner(signer);
setIsConnected(true);
return;
}
}
setSigner(null);
setIsConnected(false);
} catch (err) {
console.error("Error checking wallet:", err);
setSigner(null);
setIsConnected(false);
}
};
checkWallet();
// Listen for account changes
if (window.ethereum) {
window.ethereum.on('accountsChanged', checkWallet);
window.ethereum.on('chainChanged', checkWallet);
}
return () => {
if (window.ethereum) {
window.ethereum.removeListener('accountsChanged', checkWallet);
window.ethereum.removeListener('chainChanged', checkWallet);
}
};
}, []);
const initializeRLN = useCallback(async () => {
console.log("InitializeRLN called. Connected:", isConnected, "Signer available:", !!signer);
if (!isConnected || !signer) {
console.log("Cannot initialize RLN: Wallet not connected or signer not available.");
setError("Wallet not connected. Please connect your wallet.");
setIsLoading(false);
return;
}
setIsLoading(true);
setError(null);
try {
let currentRln = rln;
if (!currentRln) {
console.log("Creating RLN instance...");
try {
currentRln = new RLNCredentialsManager();
setRln(currentRln);
setIsInitialized(true);
console.log("RLN instance created successfully.");
} catch (createErr) {
console.error("Error creating RLN instance:", createErr);
setError(createErr instanceof Error ? createErr.message : 'Failed to create RLN instance');
setIsLoading(false);
return;
}
} else {
console.log("RLN instance already exists, skipping creation.");
}
if (currentRln && !isStarted) {
console.log("Starting RLN with signer...");
try {
await currentRln.start({ signer });
setIsStarted(true);
console.log("RLN started successfully.");
if (currentRln.contract) {
try {
const minLimit = await currentRln.contract.getMinRateLimit();
const maxLimit = await currentRln.contract.getMaxRateLimit();
if (minLimit !== undefined && maxLimit !== undefined) {
setRateMinLimit(minLimit);
setRateMaxLimit(maxLimit);
console.log("Rate limits fetched:", { min: minLimit, max: maxLimit });
} else {
console.warn("Could not fetch rate limits: undefined values returned.");
}
} catch (limitErr) {
console.warn("Could not fetch rate limits after start:", limitErr);
// Don't fail initialization for this, but log it.
}
} else {
console.warn("RLN contract not available after start, cannot fetch rate limits.");
}
} catch (startErr) {
console.error("Error starting RLN:", startErr);
setError(startErr instanceof Error ? startErr.message : 'Failed to start RLN');
setIsStarted(false);
}
} else if (isStarted) {
console.log("RLN already started.");
}
} catch (err) {
console.error('Error in initializeRLN:', err);
setError(err instanceof Error ? err.message : 'Failed to initialize RLN');
} finally {
setIsLoading(false);
}
}, [isConnected, signer, rln, isStarted]);
// Initialization logic
const state = { rln, isInitialized, isStarted, error, isLoading, rateMinLimit, rateMaxLimit };
const actions = { setRln, setIsInitialized, setIsStarted, setError, setIsLoading, setRateMinLimit, setRateMaxLimit };
const { initializeRLN, initializationInProgress, hasInitialized } = useRLNInitialization(
state, actions, isConnected, signer
);
// Auto-initialize effect for Light implementation
// Auto-initialize effect
useEffect(() => {
console.log('Auto-init check:', {
isConnected,
hasSigner: !!signer,
isInitialized,
isStarted,
isLoading
isLoading,
initInProgress: initializationInProgress,
hasInitialized
});
if (isConnected && signer && !isInitialized && !isStarted && !isLoading) {
if (isConnected &&
signer &&
!isInitialized &&
!isStarted &&
!isLoading &&
!initializationInProgress &&
!hasInitialized) {
console.log('Auto-initializing Light RLN implementation...');
initializeRLN();
}
}, [isConnected, signer, isInitialized, isStarted, isLoading, initializeRLN]);
}, [isConnected, signer, isInitialized, isStarted, isLoading, initializationInProgress, hasInitialized, initializeRLN]);
const getCurrentRateLimit = async (): Promise<number | null> => {
try {
if (!rln || !rln.contract || !isStarted) {
console.log("Cannot get rate limit: RLN not initialized or started");
return null;
}
const rateLimit = rln.contract.getRateLimit();
console.log("Current rate limit:", rateLimit);
return rateLimit;
} catch (err) {
console.error("Error getting current rate limit:", err);
return null;
}
};
const getRateLimitsBounds = async () => {
try {
if (!rln || !isStarted) {
return {
success: false,
rateMinLimit: 0,
rateMaxLimit: 0,
error: 'RLN not initialized or not started'
};
}
const minLimit = await rln.contract?.getMinRateLimit();
const maxLimit = await rln.contract?.getMaxRateLimit();
if (minLimit !== undefined && maxLimit !== undefined) {
// Update state
setRateMinLimit(minLimit);
setRateMaxLimit(maxLimit);
} else {
throw new Error("Rate limits not available");
}
return {
success: true,
rateMinLimit: minLimit,
rateMaxLimit: maxLimit
};
} catch (err) {
return {
success: false,
rateMinLimit: rateMinLimit,
rateMaxLimit: rateMaxLimit,
error: err instanceof Error ? err.message : 'Failed to get rate limits'
};
}
};
const saveCredentialsToKeystore = async (credentials: KeystoreEntity, password: string): Promise<string> => {
try {
return await saveToKeystore(credentials, password);
} catch (err) {
console.error("Error saving credentials to keystore:", err);
throw err;
}
};
const registerMembership = async (rateLimit: number, saveOptions?: { password: string }) => {
console.log("registerMembership called with rate limit:", rateLimit);
if (!rln || !isStarted) {
return { success: false, error: 'RLN not initialized or not started' };
}
if (!signer) {
return { success: false, error: 'No signer available' };
}
try {
// Validate rate limit
if (rateLimit < rateMinLimit || rateLimit > rateMaxLimit) {
return {
success: false,
error: `Rate limit must be between ${rateMinLimit} and ${rateMaxLimit}`
};
}
await rln.contract?.setRateLimit(rateLimit);
// Ensure we're on the correct network
const isOnLineaSepolia = await ensureLineaSepoliaNetwork(signer);
if (!isOnLineaSepolia) {
console.warn("Could not switch to Linea Sepolia network. Registration may fail.");
}
// Get user address and contract address
const userAddress = await signer.getAddress();
if (!rln.contract || !rln.contract.address) {
return { success: false, error: "RLN contract address not available. Cannot proceed with registration." };
}
const contractAddress = rln.contract.address;
const tokenAddress = LINEA_SEPOLIA_CONFIG.tokenAddress;
// Create token contract instance
const tokenContract = new ethers.Contract(
tokenAddress,
ERC20_ABI,
signer
);
// Check token balance
const tokenBalance = await tokenContract.balanceOf(userAddress);
if (tokenBalance.isZero()) {
return { success: false, error: "You need tokens to register a membership. Your token balance is zero." };
}
// Check and approve token allowance if needed
const currentAllowance = await tokenContract.allowance(userAddress, contractAddress);
if (currentAllowance.eq(0)) {
console.log("Requesting token approval...");
// Approve a large amount (max uint256)
const maxUint256 = ethers.constants.MaxUint256;
try {
const approveTx = await tokenContract.approve(contractAddress, maxUint256);
console.log("Approval transaction submitted:", approveTx.hash);
// Wait for the transaction to be mined
await approveTx.wait(1);
console.log("Token approval confirmed");
} catch (approvalErr) {
console.error("Error during token approval:", approvalErr);
return {
success: false,
error: `Failed to approve token: ${approvalErr instanceof Error ? approvalErr.message : String(approvalErr)}`
};
}
} else {
console.log("Token allowance already sufficient");
}
// Generate signature for identity
const timestamp = Date.now();
const message = `Sign this message to generate your RLN credentials ${timestamp}`;
const signature = await signer.signMessage(message);
// Register membership
console.log("Registering membership...");
const credentials = await rln.registerMembership({
signature: signature
});
console.log("Credentials:", credentials);
// If we have save options, save to keystore
let keystoreHash: string | undefined;
if (saveOptions && saveOptions.password && credentials) {
try {
const credentialsEntity = credentials as KeystoreEntity;
keystoreHash = await saveCredentialsToKeystore(credentialsEntity, saveOptions.password);
console.log("Credentials saved to keystore with hash:", keystoreHash);
} catch (saveErr) {
console.error("Error saving credentials to keystore:", saveErr);
// Continue without failing the overall registration
}
}
return {
success: true,
credentials: credentials as KeystoreEntity,
keystoreHash
};
} catch (err) {
console.error("Error registering membership:", err);
let errorMsg = "Failed to register membership";
if (err instanceof Error) {
errorMsg = err.message;
}
return { success: false, error: errorMsg };
}
};
const getMembershipInfo = async (hash: string, password: string) => {
if (!rln || !rln.contract) {
throw new Error('RLN not initialized or contract not available');
}
const credential = await getDecryptedCredential(hash, password);
if (!credential) {
throw new Error('Could not decrypt credential');
}
try {
const membershipInfo = await rln.contract.getMembershipInfo(credential.identity.IDCommitmentBigInt);
if (!membershipInfo) {
throw new Error('Could not fetch membership info');
}
return {
...membershipInfo,
address: rln.contract.address,
chainId: LINEA_SEPOLIA_CONFIG.chainId.toString(),
treeIndex: Number(membershipInfo.index.toString()),
rateLimit: Number(membershipInfo.rateLimit.toString())
}
} catch (error) {
console.log("error", error);
throw error;
}
};
const extendMembership = async (hash: string, password: string) => {
try {
if (!rln || !rln.contract) {
throw new Error('RLN not initialized or contract not available');
}
const credential = await getDecryptedCredential(hash, password);
if (!credential) {
throw new Error('Could not decrypt credential');
}
await rln.contract.extendMembership(credential.identity.IDCommitmentBigInt);
return { success: true };
} catch (err) {
console.error('Error extending membership:', err);
return {
success: false,
error: err instanceof Error ? err.message : 'Failed to extend membership'
};
}
};
const eraseMembership = async (hash: string, password: string) => {
try {
if (!rln || !rln.contract) {
throw new Error('RLN not initialized or contract not available');
}
const credential = await getDecryptedCredential(hash, password);
if (!credential) {
throw new Error('Could not decrypt credential');
}
await rln.contract.eraseMembership(credential.identity.IDCommitmentBigInt);
return { success: true };
} catch (err) {
console.error('Error erasing membership:', err);
return {
success: false,
error: err instanceof Error ? err.message : 'Failed to erase membership'
};
}
};
const withdrawDeposit = async (hash: string, password: string) => {
try {
if (!rln || !rln.contract) {
throw new Error('RLN not initialized or contract not available');
}
const credential = await getDecryptedCredential(hash, password);
if (!credential) {
throw new Error('Could not decrypt credential');
}
// Get token address from config
const tokenAddress = LINEA_SEPOLIA_CONFIG.tokenAddress;
const userAddress = await signer?.getAddress();
if (!userAddress) {
throw new Error('No signer available');
}
// Call withdraw with token address and holder
await rln.contract.withdraw(tokenAddress, userAddress);
return { success: true };
} catch (err) {
console.error('Error withdrawing deposit:', err);
return {
success: false,
error: err instanceof Error ? err.message : 'Failed to withdraw deposit'
};
}
};
const getPriceForRateLimit = async (rateLimit: number): Promise<{ price: string }> => {
try {
if (!rln || !rln.contract || !isStarted) {
throw new Error('RLN not initialized or contract not available');
}
const result = await rln.contract.getPriceForRateLimit(rateLimit);
const formatted = ethers.utils.formatUnits(result.price, 18);
return { price: formatted };
} catch (err) {
console.error('Error getting price for rate limit:', err);
throw err;
}
};
// Cleanup on unmount
useEffect(() => {
return () => {
console.log("RLN Provider unmounting");
};
}, []);
return (
<RLNContext.Provider
@ -488,18 +81,30 @@ export function RLNProvider({ children }: { children: ReactNode }) {
isStarted,
error,
initializeRLN,
registerMembership,
extendMembership,
eraseMembership,
withdrawDeposit,
getMembershipInfo,
registerMembership: (rateLimit, saveOptions) =>
registerMembership(rln, isStarted, signer, rateLimit, rateMinLimit, rateMaxLimit, saveToKeystore, saveOptions),
extendMembership: (hash, password) =>
extendMembership(rln, hash, password, getDecryptedCredential),
eraseMembership: (hash, password) =>
eraseMembership(rln, hash, password, getDecryptedCredential),
withdrawDeposit: (hash, password) =>
withdrawDeposit(rln, signer, hash, password, getDecryptedCredential),
getMembershipInfo: (hash, password) =>
getMembershipInfo(rln, hash, password, getDecryptedCredential),
rateMinLimit,
rateMaxLimit,
getCurrentRateLimit,
getRateLimitsBounds,
getCurrentRateLimit: () => getCurrentRateLimit(rln, isStarted),
getRateLimitsBounds: async () => {
const result = await getRateLimitsBounds(rln, isStarted, rateMinLimit, rateMaxLimit);
if (result.success) {
setRateMinLimit(result.rateMinLimit);
setRateMaxLimit(result.rateMaxLimit);
}
return result;
},
saveCredentialsToKeystore: saveToKeystore,
isLoading,
getPriceForRateLimit
getPriceForRateLimit: (rateLimit) => getPriceForRateLimit(rln, isStarted, rateLimit)
}}
>
{children}

View File

@ -1 +1,40 @@
export { RLNProvider, useRLN } from './RLNContext';
// Main context exports
export { RLNProvider, useRLN } from './RLNContext';
// Type exports
export type {
RLNContextType,
RateLimitBounds,
RegistrationResult,
OperationResult,
MembershipInfoExtended,
PriceResult
} from './types';
// Singleton exports
export { getOrCreateRLNInstance, resetGlobalRLNInstance } from './singleton';
// Wallet exports
export { useWallet, ensureCorrectNetwork, getUserAddress } from './wallet';
export type { WalletState, UseWalletReturn } from './wallet';
// Initialization exports
export { useRLNInitialization } from './initialization';
export type { InitializationState, InitializationActions } from './initialization';
// Rate limit exports
export {
getCurrentRateLimit,
getRateLimitsBounds,
getPriceForRateLimit,
validateRateLimit
} from './rateLimits';
// Operations exports
export {
registerMembership,
getMembershipInfo,
extendMembership,
eraseMembership,
withdrawDeposit
} from './operations';

View File

@ -0,0 +1,212 @@
"use client";
import { useCallback, useRef } from 'react';
import { RLNInstance } from '@waku/rln';
import { ethers } from 'ethers';
import { getOrCreateRLNInstance } from './singleton';
export interface InitializationState {
rln: RLNInstance | null;
isInitialized: boolean;
isStarted: boolean;
error: string | null;
isLoading: boolean;
rateMinLimit: number;
rateMaxLimit: number;
}
export interface InitializationActions {
setRln: (rln: RLNInstance | null) => void;
setIsInitialized: (initialized: boolean) => void;
setIsStarted: (started: boolean) => void;
setError: (error: string | null) => void;
setIsLoading: (loading: boolean) => void;
setRateMinLimit: (limit: number) => void;
setRateMaxLimit: (limit: number) => void;
}
export const useRLNInitialization = (
state: InitializationState,
actions: InitializationActions,
isConnected: boolean,
signer: ethers.Signer | null
) => {
// Use refs to prevent race conditions and ensure single initialization
const initializationInProgress = useRef(false);
const hasInitialized = useRef(false);
const lastInitAttempt = useRef(0);
const retryCount = useRef(0);
const maxRetries = 3;
const retryDelay = 1000; // Start with 1 second delay
const initializeRLN = useCallback(async () => {
console.log("InitializeRLN called. Connected:", isConnected, "Signer available:", !!signer);
// Prevent multiple simultaneous initialization attempts
if (initializationInProgress.current) {
console.log("Initialization already in progress, skipping...");
return;
}
// Prevent rapid retry attempts
const now = Date.now();
if (now - lastInitAttempt.current < retryDelay) {
console.log("Too soon since last attempt, skipping...");
return;
}
lastInitAttempt.current = now;
if (!isConnected || !signer) {
console.log("Cannot initialize RLN: Wallet not connected or signer not available.");
actions.setError("Wallet not connected. Please connect your wallet.");
return;
}
// If already initialized and started, no need to reinitialize
if (state.isInitialized && state.isStarted && state.rln) {
console.log("RLN already initialized and started");
return;
}
// If we've already successfully initialized once, don't reinitialize
if (hasInitialized.current && state.rln) {
console.log("RLN already initialized once, reusing existing instance");
if (!state.isStarted) {
try {
await state.rln.start({ signer });
actions.setIsStarted(true);
console.log("RLN restarted successfully.");
} catch (startErr) {
console.error("Error restarting RLN:", startErr);
actions.setError(startErr instanceof Error ? startErr.message : 'Failed to restart RLN');
}
}
return;
}
initializationInProgress.current = true;
actions.setIsLoading(true);
actions.setError(null);
try {
let currentRln = state.rln;
// Cleanup existing instance before creating new one
if (currentRln) {
console.log("Cleaning up existing RLN instance...");
actions.setRln(null);
actions.setIsInitialized(false);
actions.setIsStarted(false);
currentRln = null;
// Small delay to ensure cleanup is complete
await new Promise(resolve => setTimeout(resolve, 100));
}
if (!currentRln) {
console.log("Creating new RLN instance...");
try {
// Add a small delay before WASM initialization to prevent rapid calls
if (retryCount.current > 0) {
const delay = retryDelay * Math.pow(2, retryCount.current - 1); // Exponential backoff
console.log(`Waiting ${delay}ms before retry attempt ${retryCount.current}...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
// Ensure we're not in a rapid initialization loop
await new Promise(resolve => setTimeout(resolve, 50));
// Use singleton pattern to ensure WASM is only initialized once
console.time("RLN WASM BLOB");
currentRln = await getOrCreateRLNInstance();
console.timeEnd("RLN WASM BLOB");
actions.setRln(currentRln);
actions.setIsInitialized(true);
hasInitialized.current = true; // Mark as successfully initialized
retryCount.current = 0; // Reset retry count on success
console.log("RLN instance created successfully.");
} catch (createErr) {
console.error("Error creating RLN instance:", createErr);
// Check if this is a WASM-related error and implement retry logic
const errorMessage = createErr instanceof Error ? createErr.message : String(createErr);
const isWasmError = errorMessage.includes('WebAssembly') ||
errorMessage.includes('wasm') ||
errorMessage.includes('Table.grow');
if (isWasmError && retryCount.current < maxRetries) {
retryCount.current++;
console.log(`WASM error detected, retrying... (attempt ${retryCount.current}/${maxRetries})`);
actions.setError(`Initializing RLN (attempt ${retryCount.current}/${maxRetries})...`);
// Release the lock and retry after a delay
initializationInProgress.current = false;
actions.setIsLoading(false);
// Retry with exponential backoff
setTimeout(() => {
initializeRLN();
}, retryDelay * Math.pow(2, retryCount.current - 1));
return;
}
actions.setError(errorMessage);
actions.setIsLoading(false);
initializationInProgress.current = false;
return;
}
} else {
console.log("RLN instance already exists, skipping creation.");
}
if (currentRln && !state.isStarted) {
console.log("Starting RLN with signer...");
try {
await currentRln.start({ signer });
actions.setIsStarted(true);
console.log("RLN started successfully.");
if (currentRln.contract) {
try {
const minLimit = await currentRln.contract.getMinRateLimit();
const maxLimit = await currentRln.contract.getMaxRateLimit();
if (minLimit !== undefined && maxLimit !== undefined) {
actions.setRateMinLimit(minLimit);
actions.setRateMaxLimit(maxLimit);
console.log("Rate limits fetched:", { min: minLimit, max: maxLimit });
} else {
console.warn("Could not fetch rate limits: undefined values returned.");
}
} catch (limitErr) {
console.warn("Could not fetch rate limits after start:", limitErr);
// Don't fail initialization for this, but log it.
}
} else {
console.warn("RLN contract not available after start, cannot fetch rate limits.");
}
} catch (startErr) {
console.error("Error starting RLN:", startErr);
actions.setError(startErr instanceof Error ? startErr.message : 'Failed to start RLN');
actions.setIsStarted(false);
}
} else if (state.isStarted) {
console.log("RLN already started.");
}
} catch (err) {
console.error('Error in initializeRLN:', err);
actions.setError(err instanceof Error ? err.message : 'Failed to initialize RLN');
} finally {
actions.setIsLoading(false);
initializationInProgress.current = false;
}
}, [isConnected, signer, state.rln, state.isStarted, state.isInitialized, actions]);
return {
initializeRLN,
initializationInProgress: initializationInProgress.current,
hasInitialized: hasInitialized.current
};
};

View File

@ -0,0 +1,271 @@
import { ethers } from 'ethers';
import { RLNInstance, KeystoreEntity } from '@waku/rln';
import { ERC20_ABI, LINEA_SEPOLIA_CONFIG } from '../../utils/network';
import { RegistrationResult, OperationResult, MembershipInfoExtended } from './types';
import { ensureCorrectNetwork, getUserAddress } from './wallet';
import { validateRateLimit } from './rateLimits';
/**
* Register a new RLN membership
*/
export const registerMembership = async (
rln: RLNInstance | null,
isStarted: boolean,
signer: ethers.Signer | null,
rateLimit: number,
rateMinLimit: number,
rateMaxLimit: number,
saveCredentialsToKeystore: (credentials: KeystoreEntity, password: string) => Promise<string>,
saveOptions?: { password: string }
): Promise<RegistrationResult> => {
console.log("registerMembership called with rate limit:", rateLimit);
if (!rln || !isStarted) {
return { success: false, error: 'RLN not initialized or not started' };
}
if (!signer) {
return { success: false, error: 'No signer available' };
}
try {
// Validate rate limit
const validation = validateRateLimit(rateLimit, rateMinLimit, rateMaxLimit);
if (!validation.isValid) {
return { success: false, error: validation.error };
}
await rln.contract?.setRateLimit(rateLimit);
// Ensure we're on the correct network
const isOnLineaSepolia = await ensureCorrectNetwork(signer);
if (!isOnLineaSepolia) {
console.warn("Could not switch to Linea Sepolia network. Registration may fail.");
}
// Get user address and contract address
const userAddress = await getUserAddress(signer);
if (!rln.contract || !rln.contract.address) {
return { success: false, error: "RLN contract address not available. Cannot proceed with registration." };
}
const contractAddress = rln.contract.address;
const tokenAddress = LINEA_SEPOLIA_CONFIG.tokenAddress;
// Create token contract instance
const tokenContract = new ethers.Contract(
tokenAddress,
ERC20_ABI,
signer
);
// Check token balance
const tokenBalance = await tokenContract.balanceOf(userAddress);
if (tokenBalance.isZero()) {
return { success: false, error: "You need tokens to register a membership. Your token balance is zero." };
}
// Check and approve token allowance if needed
const currentAllowance = await tokenContract.allowance(userAddress, contractAddress);
if (currentAllowance.eq(0)) {
console.log("Requesting token approval...");
// Approve a large amount (max uint256)
const maxUint256 = ethers.constants.MaxUint256;
try {
const approveTx = await tokenContract.approve(contractAddress, maxUint256);
console.log("Approval transaction submitted:", approveTx.hash);
// Wait for the transaction to be mined
await approveTx.wait(1);
console.log("Token approval confirmed");
} catch (approvalErr) {
console.error("Error during token approval:", approvalErr);
return {
success: false,
error: `Failed to approve token: ${approvalErr instanceof Error ? approvalErr.message : String(approvalErr)}`
};
}
} else {
console.log("Token allowance already sufficient");
}
// Generate signature for identity
const timestamp = Date.now();
const message = `Sign this message to generate your RLN credentials ${timestamp}`;
const signature = await signer.signMessage(message);
// Register membership
console.log("Registering membership...");
console.log({signature})
const credentials = await rln.registerMembership({
signature: signature
});
console.log("Credentials:", credentials);
// If we have save options, save to keystore
let keystoreHash: string | undefined;
if (saveOptions && saveOptions.password && credentials) {
try {
const credentialsEntity = credentials as KeystoreEntity;
keystoreHash = await saveCredentialsToKeystore(credentialsEntity, saveOptions.password);
console.log("Credentials saved to keystore with hash:", keystoreHash);
} catch (saveErr) {
console.error("Error saving credentials to keystore:", saveErr);
// Continue without failing the overall registration
}
}
return {
success: true,
credentials: credentials as KeystoreEntity,
keystoreHash
};
} catch (err) {
console.error("Error registering membership:", err);
let errorMsg = "Failed to register membership";
if (err instanceof Error) {
errorMsg = err.message;
}
return { success: false, error: errorMsg };
}
};
/**
* Get membership information
*/
export const getMembershipInfo = async (
rln: RLNInstance | null,
hash: string,
password: string,
getDecryptedCredential: (hash: string, password: string) => Promise<KeystoreEntity | null>
): Promise<MembershipInfoExtended> => {
if (!rln || !rln.contract) {
throw new Error('RLN not initialized or contract not available');
}
const credential = await getDecryptedCredential(hash, password);
if (!credential) {
throw new Error('Could not decrypt credential');
}
try {
const membershipInfo = await rln.contract.getMembershipInfo(credential.identity.IDCommitmentBigInt);
if (!membershipInfo) {
throw new Error('Could not fetch membership info');
}
return {
...membershipInfo,
address: rln.contract.address,
chainId: LINEA_SEPOLIA_CONFIG.chainId.toString(),
treeIndex: Number(membershipInfo.index.toString()),
rateLimit: Number(membershipInfo.rateLimit.toString())
}
} catch (error) {
console.log("error", error);
throw error;
}
};
/**
* Extend membership
*/
export const extendMembership = async (
rln: RLNInstance | null,
hash: string,
password: string,
getDecryptedCredential: (hash: string, password: string) => Promise<KeystoreEntity | null>
): Promise<OperationResult> => {
try {
if (!rln || !rln.contract) {
throw new Error('RLN not initialized or contract not available');
}
const credential = await getDecryptedCredential(hash, password);
if (!credential) {
throw new Error('Could not decrypt credential');
}
await rln.contract.extendMembership(credential.identity.IDCommitmentBigInt);
return { success: true };
} catch (err) {
console.error('Error extending membership:', err);
return {
success: false,
error: err instanceof Error ? err.message : 'Failed to extend membership'
};
}
};
/**
* Erase membership
*/
export const eraseMembership = async (
rln: RLNInstance | null,
hash: string,
password: string,
getDecryptedCredential: (hash: string, password: string) => Promise<KeystoreEntity | null>
): Promise<OperationResult> => {
try {
if (!rln || !rln.contract) {
throw new Error('RLN not initialized or contract not available');
}
const credential = await getDecryptedCredential(hash, password);
if (!credential) {
throw new Error('Could not decrypt credential');
}
await rln.contract.eraseMembership(credential.identity.IDCommitmentBigInt);
return { success: true };
} catch (err) {
console.error('Error erasing membership:', err);
return {
success: false,
error: err instanceof Error ? err.message : 'Failed to erase membership'
};
}
};
/**
* Withdraw deposit
*/
export const withdrawDeposit = async (
rln: RLNInstance | null,
signer: ethers.Signer | null,
hash: string,
password: string,
getDecryptedCredential: (hash: string, password: string) => Promise<KeystoreEntity | null>
): Promise<OperationResult> => {
try {
if (!rln || !rln.contract) {
throw new Error('RLN not initialized or contract not available');
}
const credential = await getDecryptedCredential(hash, password);
if (!credential) {
throw new Error('Could not decrypt credential');
}
// Get token address from config
const tokenAddress = LINEA_SEPOLIA_CONFIG.tokenAddress;
const userAddress = await signer?.getAddress();
if (!userAddress) {
throw new Error('No signer available');
}
// Call withdraw with token address and holder
await rln.contract.withdraw(tokenAddress, userAddress);
return { success: true };
} catch (err) {
console.error('Error withdrawing deposit:', err);
return {
success: false,
error: err instanceof Error ? err.message : 'Failed to withdraw deposit'
};
}
};

View File

@ -0,0 +1,108 @@
import { RLNInstance } from '@waku/rln';
import { ethers } from 'ethers';
import { RateLimitBounds, PriceResult } from './types';
/**
* Get current rate limit from RLN contract
*/
export const getCurrentRateLimit = async (rln: RLNInstance | null, isStarted: boolean): Promise<number | null> => {
try {
if (!rln || !rln.contract || !isStarted) {
console.log("Cannot get rate limit: RLN not initialized or started");
return null;
}
const rateLimit = rln.contract.getRateLimit();
console.log("Current rate limit:", rateLimit);
return rateLimit;
} catch (err) {
console.error("Error getting current rate limit:", err);
return null;
}
};
/**
* Get rate limit bounds from RLN contract
*/
export const getRateLimitsBounds = async (
rln: RLNInstance | null,
isStarted: boolean,
currentRateMinLimit: number,
currentRateMaxLimit: number
): Promise<RateLimitBounds> => {
try {
if (!rln || !isStarted) {
return {
success: false,
rateMinLimit: 0,
rateMaxLimit: 0,
error: 'RLN not initialized or not started'
};
}
const minLimit = await rln.contract?.getMinRateLimit();
const maxLimit = await rln.contract?.getMaxRateLimit();
if (minLimit !== undefined && maxLimit !== undefined) {
return {
success: true,
rateMinLimit: minLimit,
rateMaxLimit: maxLimit
};
} else {
throw new Error("Rate limits not available");
}
} catch (err) {
return {
success: false,
rateMinLimit: currentRateMinLimit,
rateMaxLimit: currentRateMaxLimit,
error: err instanceof Error ? err.message : 'Failed to get rate limits'
};
}
};
/**
* Get price for a specific rate limit
*/
export const getPriceForRateLimit = async (
rln: RLNInstance | null,
isStarted: boolean,
rateLimit: number
): Promise<PriceResult> => {
try {
if (!rln || !rln.contract || !isStarted) {
throw new Error('RLN not initialized or contract not available');
}
const result = await rln.contract.getPriceForRateLimit(rateLimit);
// Handle null case to fix linter error
if (!result || !result.price) {
throw new Error('Price not available for this rate limit');
}
const formatted = ethers.utils.formatUnits(result.price, 18);
return { price: formatted };
} catch (err) {
console.error('Error getting price for rate limit:', err);
throw err;
}
};
/**
* Validate rate limit is within bounds
*/
export const validateRateLimit = (
rateLimit: number,
minLimit: number,
maxLimit: number
): { isValid: boolean; error?: string } => {
if (rateLimit < minLimit || rateLimit > maxLimit) {
return {
isValid: false,
error: `Rate limit must be between ${minLimit} and ${maxLimit}`
};
}
return { isValid: true };
};

View File

@ -0,0 +1,44 @@
import { RLNInstance, createRLN } from '@waku/rln';
// Global singleton to ensure WASM is only initialized once
let globalRLNInstance: RLNInstance | null = null;
let globalInitPromise: Promise<RLNInstance> | null = null;
/**
* Singleton function to ensure WASM is only initialized once
* Handles retry logic for WASM-related errors
*/
export const getOrCreateRLNInstance = async (): Promise<RLNInstance> => {
if (globalRLNInstance) {
console.log("Reusing existing global RLN instance");
return globalRLNInstance;
}
if (globalInitPromise) {
console.log("Waiting for existing RLN initialization...");
return globalInitPromise;
}
console.log("Creating new global RLN instance...");
globalInitPromise = createRLN();
try {
globalRLNInstance = await globalInitPromise;
console.log("Global RLN instance created successfully");
return globalRLNInstance;
} catch (error) {
console.error("Error creating global RLN instance:", error);
globalInitPromise = null;
throw error;
}
};
/**
* Cleanup function to reset the global instance
* Useful for testing or when a fresh instance is needed
*/
export const resetGlobalRLNInstance = (): void => {
globalRLNInstance = null;
globalInitPromise = null;
console.log("Global RLN instance reset");
};

View File

@ -1,12 +1,61 @@
import { DecryptedCredentials, RLNCredentialsManager, RLNInstance } from "@waku/rln";
import { RLNInstance, KeystoreEntity, MembershipInfo } from "@waku/rln";
export interface RLNContextType {
rln: RLNInstance | RLNCredentialsManager | null;
isInitialized: boolean;
isStarted: boolean;
error: string | null;
initializeRLN: () => Promise<void>;
registerMembership: (rateLimit: number) => Promise<{ success: boolean; error?: string; credentials?: DecryptedCredentials }>;
rateMinLimit: number;
rateMaxLimit: number;
}
rln: RLNInstance | null;
isInitialized: boolean;
isStarted: boolean;
error: string | null;
initializeRLN: () => Promise<void>;
registerMembership: (rateLimit: number, saveOptions?: { password: string }) => Promise<{
success: boolean;
error?: string;
credentials?: KeystoreEntity;
keystoreHash?: string;
}>;
extendMembership: (hash: string, password: string) => Promise<{ success: boolean; error?: string }>;
eraseMembership: (hash: string, password: string) => Promise<{ success: boolean; error?: string }>;
withdrawDeposit: (hash: string, password: string) => Promise<{ success: boolean; error?: string }>;
getMembershipInfo: (hash: string, password: string) => Promise<MembershipInfo & {
address: string;
chainId: string;
treeIndex: number;
rateLimit: number;
}>;
rateMinLimit: number;
rateMaxLimit: number;
getCurrentRateLimit: () => Promise<number | null>;
getRateLimitsBounds: () => Promise<{ success: boolean; rateMinLimit: number; rateMaxLimit: number; error?: string }>;
saveCredentialsToKeystore: (credentials: KeystoreEntity, password: string) => Promise<string>;
isLoading: boolean;
getPriceForRateLimit: (rateLimit: number) => Promise<{ price: string }>;
}
export interface RateLimitBounds {
success: boolean;
rateMinLimit: number;
rateMaxLimit: number;
error?: string;
}
export interface RegistrationResult {
success: boolean;
error?: string;
credentials?: KeystoreEntity;
keystoreHash?: string;
}
export interface OperationResult {
success: boolean;
error?: string;
}
export interface MembershipInfoExtended extends MembershipInfo {
address: string;
chainId: string;
treeIndex: number;
rateLimit: number;
}
export interface PriceResult {
price: string;
}

View File

@ -0,0 +1,93 @@
"use client";
import { useState, useEffect } from 'react';
import { ethers } from 'ethers';
import { ensureLineaSepoliaNetwork } from '../../utils/network';
export interface WalletState {
signer: ethers.Signer | null;
isConnected: boolean;
}
export interface UseWalletReturn extends WalletState {
checkWallet: () => Promise<void>;
}
/**
* Hook to manage wallet connection and signer state
*/
export const useWallet = (): UseWalletReturn => {
const [signer, setSigner] = useState<ethers.Signer | null>(null);
const [isConnected, setIsConnected] = useState(false);
const checkWallet = async () => {
try {
if (window.ethereum) {
const provider = new ethers.providers.Web3Provider(window.ethereum);
const accounts = await provider.listAccounts();
if (accounts.length > 0) {
const signer = provider.getSigner();
setSigner(signer);
setIsConnected(true);
return;
}
}
setSigner(null);
setIsConnected(false);
} catch (err) {
console.error("Error checking wallet:", err);
setSigner(null);
setIsConnected(false);
}
};
// Listen for wallet connection
useEffect(() => {
checkWallet();
// Listen for account changes
if (window.ethereum) {
window.ethereum.on('accountsChanged', checkWallet);
window.ethereum.on('chainChanged', checkWallet);
}
return () => {
if (window.ethereum) {
window.ethereum.removeListener('accountsChanged', checkWallet);
window.ethereum.removeListener('chainChanged', checkWallet);
}
};
}, []);
return {
signer,
isConnected,
checkWallet
};
};
/**
* Utility function to ensure wallet is on Linea Sepolia network
*/
export const ensureCorrectNetwork = async (signer: ethers.Signer): Promise<boolean> => {
try {
return await ensureLineaSepoliaNetwork(signer);
} catch (error) {
console.error("Error ensuring correct network:", error);
return false;
}
};
/**
* Utility function to get user address from signer
*/
export const getUserAddress = async (signer: ethers.Signer): Promise<string> => {
try {
return await signer.getAddress();
} catch (error) {
console.error("Error getting user address:", error);
throw new Error("Failed to get user address");
}
};

View File

@ -750,9 +750,9 @@
integrity sha512-rONPWMC7PeExE077uLE4oqWrZ1IvAfz3oH9LibVAcVCopJiA9R62uavnbEzdkVmJYI6M6Zgkbeb07+tWjlq2XA==
"@noble/curves@^1.9.1":
version "1.9.2"
resolved "https://registry.npmjs.org/@noble/curves/-/curves-1.9.2.tgz"
integrity sha512-HxngEd2XUcg9xi20JkwlLCtYwfoFw4JGkuZpT+WlsPD4gB/cxkvTD8fSsoAnphGZhFdZYKeQIPCuFlWPm1uE0g==
version "1.9.4"
resolved "https://registry.npmjs.org/@noble/curves/-/curves-1.9.4.tgz"
integrity sha512-2bKONnuM53lINoDrSmK8qP8W271ms7pygDhZt4SiLOoLwBtoHqeCFi6RG42V8zd3mLHuJFhU/Bmaqo4nX0/kBw==
dependencies:
"@noble/hashes" "1.8.0"
@ -1440,17 +1440,17 @@
resolved "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.5.0.tgz"
integrity sha512-YmocNlEcX/AgJv8gI41bhjMOTcKcea4D2nRIbZj+MhRtSH5+vEU8r/pFuTuoF+JjVplLsBueU+CILfBPVISyGQ==
"@waku/core@0.0.37-987c6cd.0":
version "0.0.37-987c6cd.0"
resolved "https://registry.npmjs.org/@waku/core/-/core-0.0.37-987c6cd.0.tgz"
integrity sha512-w3F/dzY/YA5T3l62YKKcza3fMLZNzprupoKScvtpWS8VymLIGZCMhzcKOij4lt6SWzHWyTEMa9qtcB6tZPIC+A==
"@waku/core@0.0.38-e224c05.0":
version "0.0.38-e224c05.0"
resolved "https://registry.npmjs.org/@waku/core/-/core-0.0.38-e224c05.0.tgz"
integrity sha512-VtRfwF6WGj3HhB4cywiit3KcrHYtUtoD1wjAviO8b4AzH9LbtV7NmYvGIy5ac9sOpRQEUNagOFT6+oXu7Ay+HQ==
dependencies:
"@libp2p/ping" "2.0.35"
"@noble/hashes" "^1.3.2"
"@waku/enr" "0.0.31-987c6cd.0"
"@waku/interfaces" "0.0.32-987c6cd.0"
"@waku/proto" "0.0.12-987c6cd.0"
"@waku/utils" "0.0.25-987c6cd.0"
"@waku/enr" "0.0.32-e224c05.0"
"@waku/interfaces" "0.0.33-e224c05.0"
"@waku/proto" "0.0.13-e224c05.0"
"@waku/utils" "0.0.26-e224c05.0"
debug "^4.3.4"
it-all "^3.0.4"
it-length-prefixed "^9.0.4"
@ -1458,42 +1458,42 @@
uint8arraylist "^2.4.3"
uuid "^9.0.0"
"@waku/enr@0.0.31-987c6cd.0":
version "0.0.31-987c6cd.0"
resolved "https://registry.npmjs.org/@waku/enr/-/enr-0.0.31-987c6cd.0.tgz"
integrity sha512-nStUXohULcatKLxCyzU7JJK2gKhMXYEAdN90uL1Ggl4tKA9uUrQ2YZC/WB61RmucnDlFNH6phFv47/WbARhhVA==
"@waku/enr@0.0.32-e224c05.0":
version "0.0.32-e224c05.0"
resolved "https://registry.npmjs.org/@waku/enr/-/enr-0.0.32-e224c05.0.tgz"
integrity sha512-f+DEugomxeDgCylxv2xm6eLIJBB76dctB4B/d0zUMld26zjBozRKUsaNn8DOWRT5f44WzyP+5gRqbTm4e/qsMw==
dependencies:
"@ethersproject/rlp" "^5.7.0"
"@libp2p/crypto" "5.1.6"
"@libp2p/peer-id" "5.1.7"
"@multiformats/multiaddr" "^12.0.0"
"@noble/secp256k1" "^1.7.1"
"@waku/utils" "0.0.25-987c6cd.0"
"@waku/utils" "0.0.26-e224c05.0"
debug "^4.3.4"
js-sha3 "^0.9.2"
"@waku/interfaces@0.0.32-987c6cd.0":
version "0.0.32-987c6cd.0"
resolved "https://registry.npmjs.org/@waku/interfaces/-/interfaces-0.0.32-987c6cd.0.tgz"
integrity sha512-7dfGDx1bs+rs1nlMwAZYd0Di4gLyD9cWR0ApDJ+I0sU3enn1NT6hM1U3cp+LE6xqxXUoe2tfQi/4QhrECaGypQ==
"@waku/interfaces@0.0.33-e224c05.0":
version "0.0.33-e224c05.0"
resolved "https://registry.npmjs.org/@waku/interfaces/-/interfaces-0.0.33-e224c05.0.tgz"
integrity sha512-8GH7Tg0t74R32NqQHVm0ttVPb26Cvn/p+Iiht3mha8gGHQVkYYYyECXfZYAQ9pld3y0GB+yqpn5EiZiTNs3AOQ==
"@waku/proto@0.0.12-987c6cd.0":
version "0.0.12-987c6cd.0"
resolved "https://registry.npmjs.org/@waku/proto/-/proto-0.0.12-987c6cd.0.tgz"
integrity sha512-B0u7Qkm/U6mH0ZCGYvRfg4d44JboscYBmXifeOTwY0i4QH8nvrZcPIIsUqvNmzT2CjrAwpt8H+98fGp9l4fKow==
"@waku/proto@0.0.13-e224c05.0":
version "0.0.13-e224c05.0"
resolved "https://registry.npmjs.org/@waku/proto/-/proto-0.0.13-e224c05.0.tgz"
integrity sha512-zvIGhAECY1v691tWnyiFcUAPvd06itXRq9BggN5Nwq+TKMl9XOL6+kHuTzovvn1PM9ajRyoP3zbqT1musxeixQ==
dependencies:
protons-runtime "^5.4.0"
"@waku/rln@0.1.7-987c6cd.0":
version "0.1.7-987c6cd.0"
resolved "https://registry.npmjs.org/@waku/rln/-/rln-0.1.7-987c6cd.0.tgz"
integrity sha512-6w/17WpD7jdw5LyM6OX0c2mMxW3cRrXG7wxi7PzhoSb2celNWZYhnPR9UK+rgsP5opoBDXD7LAyaEEqA3afbgA==
"@waku/rln@0.1.8-e224c05.0":
version "0.1.8-e224c05.0"
resolved "https://registry.npmjs.org/@waku/rln/-/rln-0.1.8-e224c05.0.tgz"
integrity sha512-9uSP1ARwOdpGHJ9LIpNdNo03/q60YYYuDGCX+fPfrDFooWjxvIn0+G1TzxUgJLLP6g7stxaWj+iKCCktNFpxZQ==
dependencies:
"@chainsafe/bls-keystore" "3.0.0"
"@noble/hashes" "^1.2.0"
"@waku/core" "0.0.37-987c6cd.0"
"@waku/utils" "0.0.25-987c6cd.0"
"@waku/zerokit-rln-wasm" "^0.0.13"
"@waku/core" "0.0.38-e224c05.0"
"@waku/utils" "0.0.26-e224c05.0"
"@waku/zerokit-rln-wasm" "^0.2.1"
chai "^5.1.2"
chai-as-promised "^8.0.1"
chai-spies "^1.1.0"
@ -1504,21 +1504,21 @@
sinon "^19.0.2"
uuid "^11.0.5"
"@waku/utils@0.0.25-987c6cd.0":
version "0.0.25-987c6cd.0"
resolved "https://registry.npmjs.org/@waku/utils/-/utils-0.0.25-987c6cd.0.tgz"
integrity sha512-HaYkDnVtpfmsXfDBd0gB63CxidIeE9gKMRMCaaywy6W07HK32ZgMgnD/BoPbaREa4BpEZELvW3qbYUydm3AKzw==
"@waku/utils@0.0.26-e224c05.0":
version "0.0.26-e224c05.0"
resolved "https://registry.npmjs.org/@waku/utils/-/utils-0.0.26-e224c05.0.tgz"
integrity sha512-eleBm6L5ky5xKRoCMXfFhLmvyGix8dhOXtDIS8/lIr+CJ09cDQoAHhhpPsJnLmf8DRxYQ1PDqPsEet9sa2LT9Q==
dependencies:
"@noble/hashes" "^1.3.2"
"@waku/interfaces" "0.0.32-987c6cd.0"
"@waku/interfaces" "0.0.33-e224c05.0"
chai "^4.3.10"
debug "^4.3.4"
uint8arrays "^5.0.1"
"@waku/zerokit-rln-wasm@^0.0.13":
version "0.0.13"
resolved "https://registry.npmjs.org/@waku/zerokit-rln-wasm/-/zerokit-rln-wasm-0.0.13.tgz"
integrity sha512-x7CRIIslmfCmTZc7yVp3dhLlKeLUs8ILIm9kv7+wVJ23H4pPw0Z+uH0ueLIYYfwODI6fDiwJj3S1vdFzM8D1zA==
"@waku/zerokit-rln-wasm@^0.2.1":
version "0.2.1"
resolved "https://registry.npmjs.org/@waku/zerokit-rln-wasm/-/zerokit-rln-wasm-0.2.1.tgz"
integrity sha512-2Xp7e92y4qZpsiTPGBSVr4gVJ9mJTLaudlo0DQxNpxJUBtoJKpxdH5xDCQDiorbkWZC2j9EId+ohhxHO/xC1QQ==
abort-error@^1.0.1:
version "1.0.1"
@ -1764,14 +1764,14 @@ binary-extensions@^2.0.0:
integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==
bn.js@^4.11.9:
version "4.12.1"
resolved "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz"
integrity sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==
version "4.12.2"
resolved "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz"
integrity sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==
bn.js@^5.2.1:
version "5.2.1"
resolved "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz"
integrity sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==
version "5.2.2"
resolved "https://registry.npmjs.org/bn.js/-/bn.js-5.2.2.tgz"
integrity sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw==
brace-expansion@^1.1.7:
version "1.1.11"
@ -4300,9 +4300,9 @@ queue-microtask@^1.2.2:
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
race-event@^1.3.0:
version "1.6.0"
resolved "https://registry.npmjs.org/race-event/-/race-event-1.6.0.tgz"
integrity sha512-hXkk3CDepWELBG2MsT/zIiTbjNNucMo49vwZEdjChJlxJivc8fWIu/Gh/4vEJdWsHDmnGCC6++ftP2Afep6RUg==
version "1.6.1"
resolved "https://registry.npmjs.org/race-event/-/race-event-1.6.1.tgz"
integrity sha512-vi7WH5g5KoTFpu2mme/HqZiWH14XSOtg5rfp6raBskBHl7wnmy3F/biAIyY5MsK+BHWhoPhxtZ1Y2R7OHHaWyQ==
dependencies:
abort-error "^1.0.1"