Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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<IdentityCredential>();

const [rateLimit, setRateLimit] = useState<number>(rateMinLimit);
const [isRegistering, setIsRegistering] = useState(false);
const [isInitializing, setIsInitializing] = useState(false);
Expand All @@ -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<HTMLInputElement>) => {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -139,6 +154,19 @@ export default function RLNMembershipRegistration() {
{isInitializing ? "Initializing..." : "Initialize RLN"}
</button>
)}
<button
onClick={handleReadPasskey}
disabled={isInitializing || !isLineaSepolia}
className={`px-3 py-1 text-xs rounded-md transition-colors ${
isInitializing
? "bg-gray-400 text-gray-700 cursor-not-allowed"
: isLineaSepolia
? "bg-blue-600 text-white hover:bg-blue-700"
: "bg-gray-400 text-gray-700 cursor-not-allowed"
}`}
>
Read RLN from passkey
</button>
</div>
{error && (
<p className="text-xs text-red-600 mt-1">{error}</p>
Expand Down Expand Up @@ -219,6 +247,41 @@ export default function RLNMembershipRegistration() {
</div>
</div>
)}

{identity && (
<div className="mt-3 p-3 bg-gray-100 dark:bg-gray-800 rounded-md">
<p className="font-medium mb-2">Your RLN Credentials:</p>
<div className="text-xs font-mono overflow-auto">
<h4 className="font-semibold mt-2 mb-1">Identity:</h4>
<p className="mb-1">
<span className="font-semibold">ID Commitment:</span> {Buffer.from(identity.IDCommitment).toString('hex')}
</p>
<p className="mb-1">
<span className="font-semibold">ID Secret Hash:</span> {Buffer.from(identity.IDSecretHash).toString('hex')}
</p>
<p className="mb-1">
<span className="font-semibold">ID Nullifier:</span> {Buffer.from(identity.IDNullifier).toString('hex')}
</p>
<p className="mb-3">
<span className="font-semibold">ID Trapdoor:</span> {Buffer.from(identity.IDTrapdoor).toString('hex')}
</p>

{/* <h4 className="font-semibold mt-3 mb-1">Membership:</h4>
<p className="mb-1">
<span className="font-semibold">Chain ID:</span> {registrationResult.credentials.membership.chainId}
</p>
<p className="mb-1">
<span className="font-semibold">Contract Address:</span> {registrationResult.credentials.membership.address}
</p>
<p className="mb-1">
<span className="font-semibold">Tree Index:</span> {registrationResult.credentials.membership.treeIndex}
</p> */}
</div>
<p className="text-xs mt-2 text-gray-600 dark:text-gray-400">
These credentials are your proof of membership. Store them securely.
</p>
</div>
)}

{registrationResult.success === true && (
<div className="mt-4 p-3 bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200 rounded">
Expand Down Expand Up @@ -251,7 +314,7 @@ export default function RLNMembershipRegistration() {
Your RLN membership is now registered and can be used with your Waku node.
</p>

{registrationResult.credentials && (
{(registrationResult.credentials) && (
<div className="mt-3 p-3 bg-gray-100 dark:bg-gray-800 rounded-md">
<p className="font-medium mb-2">Your RLN Credentials:</p>
<div className="text-xs font-mono overflow-auto">
Expand All @@ -269,7 +332,7 @@ export default function RLNMembershipRegistration() {
<span className="font-semibold">ID Trapdoor:</span> {Buffer.from(registrationResult.credentials.identity.IDTrapdoor).toString('hex')}
</p>

<h4 className="font-semibold mt-3 mb-1">Membership:</h4>
{/* <h4 className="font-semibold mt-3 mb-1">Membership:</h4>
<p className="mb-1">
<span className="font-semibold">Chain ID:</span> {registrationResult.credentials.membership.chainId}
</p>
Expand All @@ -278,7 +341,7 @@ export default function RLNMembershipRegistration() {
</p>
<p className="mb-1">
<span className="font-semibold">Tree Index:</span> {registrationResult.credentials.membership.treeIndex}
</p>
</p> */}
</div>
<p className="text-xs mt-2 text-gray-600 dark:text-gray-400">
These credentials are your proof of membership. Store them securely.
Expand Down
14 changes: 6 additions & 8 deletions examples/keystore-management/src/contexts/RLNContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)",
Expand All @@ -25,7 +25,7 @@ interface RLNContextType {
isStarted: boolean;
error: string | null;
initializeRLN: () => Promise<void>;
registerMembership: (rateLimit: number) => Promise<{ success: boolean; error?: string; credentials?: DecryptedCredentials }>;
registerMembership: (rateLimit: number, createPasskey: (s: ethers.Signer) => Promise<string>) => Promise<{ success: boolean; error?: string; credentials?: DecryptedCredentials }>;
rateMinLimit: number;
rateMaxLimit: number;
}
Expand Down Expand Up @@ -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<string>) => {
console.log("registerMembership called with rate limit:", rateLimit);

if (!rln || !isStarted) {
Expand Down Expand Up @@ -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");
}
Expand Down Expand Up @@ -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(() => {
Expand Down
124 changes: 124 additions & 0 deletions examples/keystore-management/src/contexts/usePasskey.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { useState, useEffect } from 'react';

export const usePasskey = () => {
const [passkeyId, setPasskeyId] = useState<string | null>(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<PublicKeyCredential | null> => {
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<string> => {
// 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
};
};
Loading