mirror of
https://github.com/logos-messaging/lab.waku.org.git
synced 2026-01-05 07:13:07 +00:00
add passkey
This commit is contained in:
parent
21692759ce
commit
e236b866af
@ -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);
|
||||
@ -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>) => {
|
||||
@ -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"}
|
||||
</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>
|
||||
@ -251,7 +279,7 @@ export default function RLNMembershipRegistration() {
|
||||
Your RLN membership is now registered and can be used with your Waku node.
|
||||
</p>
|
||||
|
||||
{registrationResult.credentials && (
|
||||
{(registrationResult.credentials && !hasPasskey()) && (
|
||||
<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">
|
||||
@ -269,7 +297,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>
|
||||
@ -278,7 +306,42 @@ export default function RLNMembershipRegistration() {
|
||||
</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>
|
||||
)}
|
||||
|
||||
{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.
|
||||
|
||||
@ -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<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;
|
||||
}
|
||||
@ -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) {
|
||||
@ -223,22 +223,20 @@ 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});
|
||||
if (!_credentials) {
|
||||
throw new Error("Failed to register membership: No credentials returned");
|
||||
}
|
||||
if (!_credentials.identity) {
|
||||
throw new Error("Failed to register membership: Missing identity information");
|
||||
}
|
||||
if (!_credentials.membership) {
|
||||
throw new Error("Failed to register membership: Missing membership information");
|
||||
}
|
||||
// const _credentials = await rln.registerMembership({signature: seed});
|
||||
// if (!_credentials) {
|
||||
// throw new Error("Failed to register membership: No credentials returned");
|
||||
// }
|
||||
// if (!_credentials.identity) {
|
||||
// throw new Error("Failed to register membership: Missing identity information");
|
||||
// }
|
||||
// if (!_credentials.membership) {
|
||||
// throw new Error("Failed to register membership: Missing membership information");
|
||||
// }
|
||||
|
||||
return { success: true, credentials: _credentials };
|
||||
return { success: true, credentials: null };
|
||||
} catch (err) {
|
||||
let errorMsg = "Failed to register membership";
|
||||
if (err instanceof Error) {
|
||||
@ -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(() => {
|
||||
|
||||
124
examples/keystore-management/src/contexts/usePasskey.ts
Normal file
124
examples/keystore-management/src/contexts/usePasskey.ts
Normal file
@ -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…
x
Reference in New Issue
Block a user