diff --git a/package-lock.json b/package-lock.json index 1be6b96..a4d264b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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" diff --git a/package.json b/package.json index 954a1a4..f4bcd3b 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/components/Tabs/MembershipTab/MembershipRegistration.tsx b/src/components/Tabs/MembershipTab/MembershipRegistration.tsx index 16764d9..f11725d 100644 --- a/src/components/Tabs/MembershipTab/MembershipRegistration.tsx +++ b/src/components/Tabs/MembershipTab/MembershipRegistration.tsx @@ -36,21 +36,28 @@ export function MembershipRegistration({ tabId: _tabId }: MembershipRegistration const isLineaSepolia = chainId === 59141; - const [price, setPrice] = useState(''); + // Store prices for both rate limits + const [prices, setPrices] = useState<{ [key: number]: string }>({}); const [priceLoading, setPriceLoading] = useState(false); const [priceError, setPriceError] = useState(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 ? ( {priceError} ) : ( - <>{rateLimit === 300 && <>Token spend: {price} WTT} + <>Token spend: {prices[300] ?? "--"} WTT )} @@ -246,14 +253,12 @@ export function MembershipRegistration({ tabId: _tabId }: MembershipRegistration ) : priceError ? ( {priceError} ) : ( - <>{rateLimit === 600 && <>Token spend: {price} WTT} + <>Token spend: {prices[600] ?? "--"} WTT )} - {/* Show calculated token spend for selected rate limit */} - {/* Removed redundant price display below, now shown in each ToggleGroupItem */}
diff --git a/src/contexts/rln/RLNContext.tsx b/src/contexts/rln/RLNContext.tsx index 3fe37cd..12339b9 100644 --- a/src/contexts/rln/RLNContext.tsx +++ b/src/contexts/rln/RLNContext.tsx @@ -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; - 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; - rateMinLimit: number; - rateMaxLimit: number; - getCurrentRateLimit: () => Promise; - getRateLimitsBounds: () => Promise<{ success: boolean; rateMinLimit: number; rateMaxLimit: number; error?: string }>; - saveCredentialsToKeystore: (credentials: KeystoreEntity, password: string) => Promise; - 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(undefined); export function RLNProvider({ children }: { children: ReactNode }) { - const [rln, setRln] = useState(null); + // State management + const [rln, setRln] = useState(null); const [isInitialized, setIsInitialized] = useState(false); const [isStarted, setIsStarted] = useState(false); const [error, setError] = useState(null); const [isLoading, setIsLoading] = useState(false); - - // Get the signer from window.ethereum - const [signer, setSigner] = useState(null); - const [isConnected, setIsConnected] = useState(false); const [rateMinLimit, setRateMinLimit] = useState(0); const [rateMaxLimit, setRateMaxLimit] = useState(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 => { - 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 => { - 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 ( + 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} diff --git a/src/contexts/rln/index.ts b/src/contexts/rln/index.ts index 2aae22e..211f87d 100644 --- a/src/contexts/rln/index.ts +++ b/src/contexts/rln/index.ts @@ -1 +1,40 @@ -export { RLNProvider, useRLN } from './RLNContext'; \ No newline at end of file +// 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'; \ No newline at end of file diff --git a/src/contexts/rln/initialization.ts b/src/contexts/rln/initialization.ts new file mode 100644 index 0000000..1f010db --- /dev/null +++ b/src/contexts/rln/initialization.ts @@ -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 + }; +}; \ No newline at end of file diff --git a/src/contexts/rln/operations.ts b/src/contexts/rln/operations.ts new file mode 100644 index 0000000..e10a659 --- /dev/null +++ b/src/contexts/rln/operations.ts @@ -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, + saveOptions?: { password: string } +): Promise => { + 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 +): Promise => { + 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 +): Promise => { + 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 +): Promise => { + 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 +): Promise => { + 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' + }; + } +}; \ No newline at end of file diff --git a/src/contexts/rln/rateLimits.ts b/src/contexts/rln/rateLimits.ts new file mode 100644 index 0000000..a76d6d3 --- /dev/null +++ b/src/contexts/rln/rateLimits.ts @@ -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 => { + 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 => { + 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 => { + 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 }; +}; \ No newline at end of file diff --git a/src/contexts/rln/singleton.ts b/src/contexts/rln/singleton.ts new file mode 100644 index 0000000..f20da33 --- /dev/null +++ b/src/contexts/rln/singleton.ts @@ -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 | null = null; + +/** + * Singleton function to ensure WASM is only initialized once + * Handles retry logic for WASM-related errors + */ +export const getOrCreateRLNInstance = async (): Promise => { + 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"); +}; \ No newline at end of file diff --git a/src/contexts/rln/types.ts b/src/contexts/rln/types.ts index 655f03d..bad5167 100644 --- a/src/contexts/rln/types.ts +++ b/src/contexts/rln/types.ts @@ -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; - registerMembership: (rateLimit: number) => Promise<{ success: boolean; error?: string; credentials?: DecryptedCredentials }>; - rateMinLimit: number; - rateMaxLimit: number; - } \ No newline at end of file + rln: RLNInstance | null; + isInitialized: boolean; + isStarted: boolean; + error: string | null; + initializeRLN: () => Promise; + 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; + rateMinLimit: number; + rateMaxLimit: number; + getCurrentRateLimit: () => Promise; + getRateLimitsBounds: () => Promise<{ success: boolean; rateMinLimit: number; rateMaxLimit: number; error?: string }>; + saveCredentialsToKeystore: (credentials: KeystoreEntity, password: string) => Promise; + 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; +} \ No newline at end of file diff --git a/src/contexts/rln/wallet.ts b/src/contexts/rln/wallet.ts new file mode 100644 index 0000000..f005674 --- /dev/null +++ b/src/contexts/rln/wallet.ts @@ -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; +} + +/** + * Hook to manage wallet connection and signer state + */ +export const useWallet = (): UseWalletReturn => { + const [signer, setSigner] = useState(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 => { + 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 => { + try { + return await signer.getAddress(); + } catch (error) { + console.error("Error getting user address:", error); + throw new Error("Failed to get user address"); + } +}; \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 7a833cb..eaecde4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"