diff --git a/src/interfaces/index.ts b/src/interfaces/index.ts index bbb83e0..7413147 100644 --- a/src/interfaces/index.ts +++ b/src/interfaces/index.ts @@ -21,8 +21,8 @@ interface Domain { version: string; } -interface AttestMessage { - attester: string; +export interface AttestMessage { + attester?: string; data: string; expirationTime: bigint; nonce: bigint; @@ -32,6 +32,13 @@ interface AttestMessage { schema: string; } +export interface RevokeMessage { + nonce: string; + revoker: string; + schema: string; + uid: string; +} + interface Signature { r: string; s: string; @@ -54,3 +61,11 @@ export interface AttestPayload { signature: Signature; types: Types; } + +export interface RevokePayload { + domain: Domain; + message: RevokeMessage; + primaryType: string; + signature: Signature; + types: Types; +} diff --git a/src/pages/Identifiers/Attestation/Attestation.tsx b/src/pages/Identifiers/Attestation/Attestation.tsx index 26235af..7ed8240 100644 --- a/src/pages/Identifiers/Attestation/Attestation.tsx +++ b/src/pages/Identifiers/Attestation/Attestation.tsx @@ -13,7 +13,7 @@ import { } from '@ethereum-attestation-service/eas-sdk'; import StepperComponent from '../../../components/shared/CustomStepper'; import { platformAuthentication } from '../../../services/api/auth'; -import { useLinkIdentifierMutation } from '../../../services/api/linking/query'; +import { useLinkIdentifierMutation } from '../../../services/api/eas/query'; import sepoliaChain from '../../../utils/contracts/eas/sepoliaChain.json'; import { useSigner } from '../../../utils/eas-wagmi-utils'; import { AttestPayload } from '../../../interfaces'; diff --git a/src/pages/Identifiers/Identifiers.tsx b/src/pages/Identifiers/Identifiers.tsx index bd1131e..7a3fe3d 100644 --- a/src/pages/Identifiers/Identifiers.tsx +++ b/src/pages/Identifiers/Identifiers.tsx @@ -11,8 +11,11 @@ import { Box, Avatar, CircularProgress, + IconButton, } from '@mui/material'; import VerifiedIcon from '@mui/icons-material/Verified'; +import VisibilityIcon from '@mui/icons-material/Visibility'; +import VisibilityOffIcon from '@mui/icons-material/VisibilityOff'; import { FaDiscord, FaGoogle } from 'react-icons/fa'; import { useNavigate } from 'react-router-dom'; import clsx from 'clsx'; @@ -26,8 +29,11 @@ import { useGetAttestations } from '../../services/eas/query'; import { decodeAttestationData, IAttestation } from '../../libs/oci'; import sepoliaChain from '../../utils/contracts/eas/sepoliaChain.json'; import { useSigner } from '../../utils/eas-wagmi-utils'; -import { useRevokeIdentifierMutation } from '../../services/api/linking/query'; -import { AttestPayload } from '../../interfaces'; +import { + useDecryptAttestationsSecretMutation, + useRevokeIdentifierMutation, +} from '../../services/api/eas/query'; +import { RevokePayload } from '../../interfaces'; import { convertStringsToBigInts } from '../../utils/helper'; interface IdentifierItemProps { @@ -40,6 +46,9 @@ interface IdentifierItemProps { onRevoke: (uid: string) => void; onConnect: (name: string) => void; isLoading: boolean; + isRevealedPending: boolean; + isRevealed: string; + onReveal: () => void; } const IdentifierItem: React.FC = ({ @@ -47,6 +56,9 @@ const IdentifierItem: React.FC = ({ onRevoke, onConnect, isLoading, + isRevealedPending, + isRevealed, + onReveal, }) => ( @@ -64,9 +76,25 @@ const IdentifierItem: React.FC = ({ primary={
{identifier.verified && ( - + )} - {identifier.name} + {identifier.name} +
+ {isRevealedPending ? ( + + ) : ( + <> + {isRevealed !== '*********' ? isRevealed : '*********'} + + {isRevealed !== '*********' ? ( + + ) : ( + + )} + + + )} +
} sx={{ ml: 2 }} @@ -119,27 +147,46 @@ export function Identifiers() { const { mutate: mutateRevokeIdentifier, data: revokeIdentifierResponse } = useRevokeIdentifierMutation(); - const [loadingIdentifier, setLoadingIdentifier] = useState(false); + const { mutate: mutateDecryptAttestationsSecret } = + useDecryptAttestationsSecretMutation(); - useEffect(() => { - if (!attestationsResponse) throw new Error('No attestations found'); + const [loadingIdentifiers, setLoadingIdentifiers] = useState<{ + [uid: string]: boolean; + }>({}); - const attestationsData = attestationsResponse.map((attestation) => { - const decodedData = decodeAttestationData(attestation.data); + const [revealedIdentifiers, setRevealedIdentifiers] = useState<{ + [uid: string]: string; + }>({}); - const providerData = decodedData.find((data) => data.name === 'provider'); + const [revealing, setRevealing] = useState<{ [uid: string]: boolean }>({}); - return { - ...attestation, - provider: - typeof providerData?.value.value === 'string' - ? providerData.value.value - : undefined, - decodedData, - }; - }); + useEffect(() => { + const processAttestations = () => { + if (!attestationsResponse) { + return; + } - setAttestations(attestationsData); + const attestationsData = attestationsResponse.map((attestation) => { + const decodedData = decodeAttestationData(attestation.data); + + const providerData = decodedData.find( + (data) => data.name === 'provider' + ); + + return { + ...attestation, + provider: + typeof providerData?.value.value === 'string' + ? providerData.value.value + : undefined, + decodedData, + }; + }); + + setAttestations(attestationsData); + }; + + processAttestations(); }, [attestationsResponse]); useEffect(() => { @@ -158,6 +205,16 @@ export function Identifiers() { }); setIdentifiers(updatedIdentifiers); + + const initialRevealedState = updatedIdentifiers.reduce( + (acc, identifier) => { + acc[identifier.uid] = '*********'; + return acc; + }, + {} as { [uid: string]: string } + ); + + setRevealedIdentifiers(initialRevealedState); }, [attestations]); const navigate = useNavigate(); @@ -168,7 +225,7 @@ export function Identifiers() { if (!siweJwt) throw new Error('OCI SIWE token not found'); - setLoadingIdentifier(true); + setLoadingIdentifiers((prev) => ({ ...prev, [uid]: true })); mutateRevokeIdentifier({ uid, @@ -181,20 +238,71 @@ export function Identifiers() { const handleConnect = useCallback( (identifier: string) => { - console.log(`Connect identifier for ${identifier}`); navigate(`/identifiers/${identifier.toLowerCase()}/attestation`); }, [navigate] ); + const handleReveal = useCallback( + (uid: string) => { + // Toggle between showing and hiding the identifier + if (revealedIdentifiers[uid] !== '*********') { + setRevealedIdentifiers((prev) => ({ + ...prev, + [uid]: '*********', + })); + return; + } + + setRevealing((prev) => ({ + ...prev, + [uid]: true, + })); + + const siweJwt = localStorage.getItem('OCI_TOKEN'); + + if (!siweJwt) throw new Error('OCI SIWE token not found'); + + mutateDecryptAttestationsSecret( + { + uid, + siweJwt, + chainId, + }, + { + onSuccess: (response) => { + console.log('Decrypted secret:', response); + + setRevealedIdentifiers((prev) => ({ + ...prev, + [uid]: response.data.id, + })); + setRevealing((prev) => ({ + ...prev, + [uid]: false, + })); + }, + onError: (error) => { + console.error('Error decrypting secret:', error); + setRevealing((prev) => ({ + ...prev, + [uid]: false, + })); + }, + } + ); + }, + [chainId, mutateDecryptAttestationsSecret, revealedIdentifiers] + ); + useEffect(() => { const revokeIdentifier = async () => { if (revokeIdentifierResponse) { console.log('Revoke identifier response', revokeIdentifierResponse); - const payload: AttestPayload = convertStringsToBigInts( + const payload: RevokePayload = convertStringsToBigInts( revokeIdentifierResponse.data - ) as AttestPayload; + ) as RevokePayload; console.log('Payload:', payload); @@ -208,28 +316,45 @@ export function Identifiers() { eas.connect(signer); - const transformedPayload: DelegatedRevocationRequest = { - schema: payload?.message?.schema, - data: { - uid: payload?.message?.uid, - }, - signature: payload.signature, - revoker: payload.message.revoker, - deadline: 0n, - }; - - const tx = await eas.revokeByDelegation(transformedPayload); - - await tx.wait(); + if ('revoker' in payload.message) { + const transformedPayload: DelegatedRevocationRequest = { + schema: payload.message.schema, + data: { + uid: payload.message.uid, + }, + signature: payload.signature, + revoker: payload.message.revoker, + deadline: 0n, + }; + + const tx = await eas.revokeByDelegation(transformedPayload); + + await tx.wait(); + + setLoadingIdentifiers((prev) => ({ + ...prev, + [payload.message.uid]: false, + })); + } else { + throw new Error('Invalid message type for revocation'); + } + } catch (error) { + console.error('Error during revocation:', error); + + if ('uid' in payload.message) { + setLoadingIdentifiers((prev) => ({ + ...prev, + [payload.message.uid]: false, + })); + } } finally { - setLoadingIdentifier(false); refetch(); } } }; revokeIdentifier(); - }, [revokeIdentifierResponse, refetch, signer]); + }, [revokeIdentifierResponse]); return (
@@ -252,7 +377,10 @@ export function Identifiers() { identifier={identifier} onRevoke={handleRevoke} onConnect={handleConnect} - isLoading={loadingIdentifier} + isLoading={loadingIdentifiers[identifier.uid] || false} + isRevealedPending={revealing[identifier.uid] || false} + isRevealed={revealedIdentifiers[identifier.uid] || '*********'} + onReveal={() => handleReveal(identifier.uid)} /> ))} diff --git a/src/services/api/linking/index.ts b/src/services/api/eas/index.ts similarity index 66% rename from src/services/api/linking/index.ts rename to src/services/api/eas/index.ts index 214f72a..ab0204d 100644 --- a/src/services/api/linking/index.ts +++ b/src/services/api/eas/index.ts @@ -12,6 +12,12 @@ export interface RevokeIdentifierParams { chainId?: number; } +export interface DecryptAttestationsSecretParams { + uid: string; + siweJwt: string; + chainId?: number; +} + export const linkIdentifier = async ({ siweJwt, anyJwt, @@ -34,3 +40,14 @@ export const revokeIdentifier = async ({ chainId, }); }; + +export const decryptAttestationsSecret = async ({ + uid, + siweJwt, + chainId, +}: DecryptAttestationsSecretParams) => { + return api.post(`/eas/${uid}/decrypt-attestation-secret`, { + siweJwt, + chainId, + }); +}; diff --git a/src/services/api/linking/query.ts b/src/services/api/eas/query.ts similarity index 65% rename from src/services/api/linking/query.ts rename to src/services/api/eas/query.ts index bd6e8e0..facf128 100644 --- a/src/services/api/linking/query.ts +++ b/src/services/api/eas/query.ts @@ -1,5 +1,7 @@ import { useMutation } from '@tanstack/react-query'; import { + decryptAttestationsSecret, + DecryptAttestationsSecretParams, linkIdentifier, LinkIdentifierParams, revokeIdentifier, @@ -35,3 +37,20 @@ export const useRevokeIdentifierMutation = () => { mutationKey: ['revokeIdentifier'], }); }; + +export const useDecryptAttestationsSecretMutation = () => { + return useMutation({ + mutationFn: async ({ + uid, + siweJwt, + chainId, + }: DecryptAttestationsSecretParams) => { + return decryptAttestationsSecret({ + uid, + siweJwt, + chainId, + }); + }, + mutationKey: ['decryptAttestationsSecret'], + }); +};