mirror of
https://github.com/logos-messaging/rln.waku.org.git
synced 2026-01-02 14:13:09 +00:00
feat: upgrade and use zerokit, cleanup
This commit is contained in:
parent
7622d02fe9
commit
cd599f0a97
110
package-lock.json
generated
110
package-lock.json
generated
@ -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"
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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';
|
||||
212
src/contexts/rln/initialization.ts
Normal file
212
src/contexts/rln/initialization.ts
Normal 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
|
||||
};
|
||||
};
|
||||
271
src/contexts/rln/operations.ts
Normal file
271
src/contexts/rln/operations.ts
Normal 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'
|
||||
};
|
||||
}
|
||||
};
|
||||
108
src/contexts/rln/rateLimits.ts
Normal file
108
src/contexts/rln/rateLimits.ts
Normal 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 };
|
||||
};
|
||||
44
src/contexts/rln/singleton.ts
Normal file
44
src/contexts/rln/singleton.ts
Normal 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");
|
||||
};
|
||||
@ -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;
|
||||
}
|
||||
93
src/contexts/rln/wallet.ts
Normal file
93
src/contexts/rln/wallet.ts
Normal 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");
|
||||
}
|
||||
};
|
||||
98
yarn.lock
98
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"
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user