diff --git a/examples/keystore-management/src/components/RLNMembershipRegistration.tsx b/examples/keystore-management/src/components/RLNMembershipRegistration.tsx index 79cdbf2..bb25ce8 100644 --- a/examples/keystore-management/src/components/RLNMembershipRegistration.tsx +++ b/examples/keystore-management/src/components/RLNMembershipRegistration.tsx @@ -1,14 +1,18 @@ "use client"; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { useRLN } from '../contexts/RLNContext'; import { useWallet } from '../contexts/WalletContext'; -import { DecryptedCredentials } from '@waku/rln'; +import { DecryptedCredentials, IdentityCredential } from '@waku/rln'; +import { usePasskey } from '@/contexts/usePasskey'; export default function RLNMembershipRegistration() { - const { registerMembership, isInitialized, isStarted, rateMinLimit, rateMaxLimit, error, initializeRLN } = useRLN(); + const { registerMembership, isInitialized, isStarted, rateMinLimit, rateMaxLimit, error, initializeRLN, rln } = useRLN(); const { isConnected, address, chainId } = useWallet(); - + const { createPasskey, hasPasskey, getPasskey, getPasskeyCredential } = usePasskey(); + + const [identity, setIdentity] = useState(); + const [rateLimit, setRateLimit] = useState(rateMinLimit); const [isRegistering, setIsRegistering] = useState(false); const [isInitializing, setIsInitializing] = useState(false); @@ -20,6 +24,17 @@ export default function RLNMembershipRegistration() { credentials?: DecryptedCredentials; }>({}); + const handleReadPasskey = () => { + if (rln && hasPasskey()) { + const seed = getPasskey(); + getPasskeyCredential(seed!); + const _identity = rln.zerokit.generateSeededIdentityCredential(seed!); + console.log(_identity); + setIdentity(_identity); + } + }; + + console.log(identity); const isLineaSepolia = chainId === 59141; const handleRateLimitChange = (e: React.ChangeEvent) => { @@ -65,7 +80,7 @@ export default function RLNMembershipRegistration() { warning: 'Please check your wallet to sign the registration message.' }); - const result = await registerMembership(rateLimit); + const result = await registerMembership(rateLimit, createPasskey); setRegistrationResult({ ...result, credentials: result.credentials @@ -139,6 +154,19 @@ export default function RLNMembershipRegistration() { {isInitializing ? "Initializing..." : "Initialize RLN"} )} + {error && (

{error}

@@ -219,6 +247,41 @@ export default function RLNMembershipRegistration() { )} + +{identity && ( +
+

Your RLN Credentials:

+
+

Identity:

+

+ ID Commitment: {Buffer.from(identity.IDCommitment).toString('hex')} +

+

+ ID Secret Hash: {Buffer.from(identity.IDSecretHash).toString('hex')} +

+

+ ID Nullifier: {Buffer.from(identity.IDNullifier).toString('hex')} +

+

+ ID Trapdoor: {Buffer.from(identity.IDTrapdoor).toString('hex')} +

+ + {/*

Membership:

+

+ Chain ID: {registrationResult.credentials.membership.chainId} +

+

+ Contract Address: {registrationResult.credentials.membership.address} +

+

+ Tree Index: {registrationResult.credentials.membership.treeIndex} +

*/} +
+

+ These credentials are your proof of membership. Store them securely. +

+
+ )} {registrationResult.success === true && (
@@ -251,7 +314,7 @@ export default function RLNMembershipRegistration() { Your RLN membership is now registered and can be used with your Waku node.

- {registrationResult.credentials && ( + {(registrationResult.credentials) && (

Your RLN Credentials:

@@ -269,7 +332,7 @@ export default function RLNMembershipRegistration() { ID Trapdoor: {Buffer.from(registrationResult.credentials.identity.IDTrapdoor).toString('hex')}

-

Membership:

+ {/*

Membership:

Chain ID: {registrationResult.credentials.membership.chainId}

@@ -278,7 +341,7 @@ export default function RLNMembershipRegistration() {

Tree Index: {registrationResult.credentials.membership.treeIndex} -

+

*/}

These credentials are your proof of membership. Store them securely. diff --git a/examples/keystore-management/src/contexts/RLNContext.tsx b/examples/keystore-management/src/contexts/RLNContext.tsx index 5ffda15..c2f02d2 100644 --- a/examples/keystore-management/src/contexts/RLNContext.tsx +++ b/examples/keystore-management/src/contexts/RLNContext.tsx @@ -6,7 +6,7 @@ import { useWallet } from './WalletContext'; import { ethers } from 'ethers'; // Constants -const SIGNATURE_MESSAGE = "Sign this message to generate your RLN credentials"; +// const SIGNATURE_MESSAGE = "Sign this message to generate your RLN credentials"; const ERC20_ABI = [ "function allowance(address owner, address spender) view returns (uint256)", "function approve(address spender, uint256 amount) returns (bool)", @@ -25,7 +25,7 @@ interface RLNContextType { isStarted: boolean; error: string | null; initializeRLN: () => Promise; - registerMembership: (rateLimit: number) => Promise<{ success: boolean; error?: string; credentials?: DecryptedCredentials }>; + registerMembership: (rateLimit: number, createPasskey: (s: ethers.Signer) => Promise) => Promise<{ success: boolean; error?: string; credentials?: DecryptedCredentials }>; rateMinLimit: number; rateMaxLimit: number; } @@ -148,7 +148,7 @@ export function RLNProvider({ children }: { children: ReactNode }) { } }; - const registerMembership = async (rateLimit: number) => { + const registerMembership = async (rateLimit: number, createPasskey: (s: ethers.Signer) => Promise) => { console.log("registerMembership called with rate limit:", rateLimit); if (!rln || !isStarted) { @@ -223,11 +223,9 @@ export function RLNProvider({ children }: { children: ReactNode }) { console.log("Token allowance already sufficient"); } - // Generate signature for identity - const message = `${SIGNATURE_MESSAGE} ${Date.now()}`; - const signature = await signer.signMessage(message); + const seed = await createPasskey(signer); - const _credentials = await rln.registerMembership({signature: signature}); + const _credentials = await rln.registerMembership({signature: seed}); if (!_credentials) { throw new Error("Failed to register membership: No credentials returned"); } @@ -258,7 +256,7 @@ export function RLNProvider({ children }: { children: ReactNode }) { } else { console.log("Wallet not connected or no signer available, skipping RLN initialization"); } - }, [isConnected, signer]); + }, [isConnected, signer, initializeRLN]); // Debug log for state changes useEffect(() => { diff --git a/examples/keystore-management/src/contexts/usePasskey.ts b/examples/keystore-management/src/contexts/usePasskey.ts new file mode 100644 index 0000000..77ae090 --- /dev/null +++ b/examples/keystore-management/src/contexts/usePasskey.ts @@ -0,0 +1,124 @@ +import { useState, useEffect } from 'react'; + +export const usePasskey = () => { + const [passkeyId, setPasskeyId] = useState(null); + + // Load passkey when component mounts + // useEffect(() => { + // const loadPasskey = async () => { + // const storedPasskeyId = localStorage.getItem('rlnPasskeyId'); + + // if (storedPasskeyId) { + // // Try to get the credential from navigator.credentials + // try { + // const credential = await getPasskeyCredential(storedPasskeyId); + // if (credential) { + // setPasskeyId(storedPasskeyId); + // } + // } catch (error) { + // console.error("Failed to retrieve passkey:", error); + // // If the credential can't be found, clear localStorage + // localStorage.removeItem('rlnPasskeyId'); + // } + // } + // }; + + // loadPasskey(); + // }, []); + + // Check if a passkey exists + const hasPasskey = (): boolean => { + return localStorage.getItem('rlnPasskeyId') !== null; + }; + + const getPasskey = (): string | null => { + const storedPasskeyId = localStorage.getItem('rlnPasskeyId'); + return storedPasskeyId; + }; + + const getPasskeyCredential = async (credentialId: string): Promise => { + try { + const idBuffer = Uint8Array.from( + atob(credentialId.replace(/-/g, '+').replace(/_/g, '/')), + c => c.charCodeAt(0) + ); + + // Create get options for the credential + const getOptions = { + publicKey: { + challenge: new Uint8Array(32), + allowCredentials: [{ + id: idBuffer, + type: 'public-key', + }], + userVerification: 'required', + timeout: 60000 + } + }; + + // Generate random values for the challenge + window.crypto.getRandomValues(getOptions.publicKey.challenge); + + // Get the credential + const credential = await navigator.credentials.get(getOptions as any) as PublicKeyCredential; + return credential; + } catch (error) { + console.error("Error retrieving passkey:", error); + return null; + } + }; + + // Create a new passkey + const createPasskey = async (signer: any): Promise => { + // Generate a random challenge for the passkey + const challenge = new Uint8Array(32); + window.crypto.getRandomValues(challenge); + + // Create credential options for the passkey + const credentialCreationOptions = { + publicKey: { + challenge: challenge, + rp: { + name: "RLN Membership", + id: window.location.hostname + }, + user: { + id: new Uint8Array([...new TextEncoder().encode(await signer.getAddress())]), + name: "RLN Membership Passkey", + displayName: "RLN Membership Passkey" + }, + pubKeyCredParams: [ + { type: "public-key", alg: -7 }, // ES256 + { type: "public-key", alg: -257 } // RS256 + ], + authenticatorSelection: { + authenticatorAttachment: "platform", + requireResidentKey: true, + userVerification: "required" + }, + timeout: 60000 + } + }; + + const credential = await navigator.credentials.create(credentialCreationOptions as any) as PublicKeyCredential; + + if (!credential) { + throw new Error("Failed to create passkey"); + } + + // Store credential ID in state and localStorage + setPasskeyId(credential.id); + localStorage.setItem('rlnPasskeyId', credential.id); + + return credential.id; + }; + + // Return the methods and state for passkey management + return { + hasPasskey, + getPasskey, + passkeyId, + createPasskey, + getPasskeyCredential + }; +}; \ No newline at end of file