Skip to content

Commit

Permalink
feat(app): displaying score attestation exipration date
Browse files Browse the repository at this point in the history
  • Loading branch information
lucianHymer committed Jun 7, 2024
1 parent 75d6f91 commit bec0c09
Show file tree
Hide file tree
Showing 8 changed files with 283 additions and 207 deletions.
13 changes: 8 additions & 5 deletions app/components/NetworkCard.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import React from "react";
import { SyncToChainButton } from "./SyncToChainButton";
import { Chain } from "../utils/chains";
import { useOnChainState } from "../hooks/useOnChainStatus";
import { useOnChainStatus } from "../hooks/useOnChainStatus";
import { OnChainStatus } from "../utils/onChainStatus";
import { useWalletStore } from "../context/walletStore";
import { useOnChainData } from "../hooks/useOnChainData";
import { Spinner } from "@chakra-ui/react";

const formatDate = (date: Date): string =>
Intl.DateTimeFormat("en-US", { month: "short", day: "2-digit", year: "numeric" }).format(date);

export function NetworkCard({ chain }: { chain: Chain }) {
// TODO
const expirationDate = new Date("1/1/2024");
const { status } = useOnChainState({ chain });
const status = useOnChainStatus({ chain });
const { expirationDate } = useOnChainData().data[chain.id] || {};
const address = useWalletStore((state) => state.address);

const isOnChain = [
Expand Down Expand Up @@ -50,12 +51,14 @@ export function NetworkCard({ chain }: { chain: Chain }) {
>
{expired ? (
"Expired"
) : (
) : expirationDate ? (
<>
Expires
<br />
{formatDate(expirationDate)}
</>
) : (
<Spinner size="sm" />
)}
</div>
</>
Expand Down
6 changes: 4 additions & 2 deletions app/components/SyncToChainButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,16 @@ export function SyncToChainButton({ onChainStatus, chain, className }: SyncToCha
return (
<button
{...props}
className={`center flex justify-center p-2 ${className} ${props.className} h-11 w-fit rounded border ${expired ? "border-focus" : "disabled:border-foreground-6 border-foreground-2"}`}
className={`center flex justify-center items-center p-2 ${className} ${props.className} h-11 w-fit rounded border ${expired ? "border-focus" : "disabled:border-foreground-6 border-foreground-2"}`}
data-testid="sync-to-chain-button"
>
<div className={`${loading ? "block" : "hidden"} relative top-1`}>
<Spinner thickness="2px" speed="0.65s" emptyColor="darkGray" color="gray" size="md" />
</div>
{needToSwitchChain && (
<Tooltip className="px-0">You will be prompted to switch to {chain.label} and sign the transaction</Tooltip>
<Tooltip className="px-0" iconClassName={expired ? "text-focus" : ""}>
You will be prompted to switch to {chain.label} and sign the transaction
</Tooltip>
)}
<span
className={`mx-1 translate-y-[1px] ${loading ? "hidden" : "block"} ${
Expand Down
4 changes: 2 additions & 2 deletions app/components/VeraxPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { Button } from "./Button";
import { ScorerContext } from "../context/scorerContext";
import { useContext, useState } from "react";
import { CustomDashboardPanel } from "./CustomDashboardPanel";
import { useOnChainState } from "../hooks/useOnChainStatus";
import { useOnChainStatus } from "../hooks/useOnChainStatus";

const VeraxLogo = () => (
<svg width="64" height="56" viewBox="0 0 64 56" fill="none" xmlns="http://www.w3.org/2000/svg">
Expand Down Expand Up @@ -41,7 +41,7 @@ const LINEA_CHAIN_NAME = process.env.NEXT_PUBLIC_ENABLE_TESTNET === "on" ? "Line
const chain = chains.find(({ label }) => label === LINEA_CHAIN_NAME);

export const VeraxPanel = ({ className }: { className: string }) => {
const { status: onChainStatus } = useOnChainState({ chain });
const onChainStatus = useOnChainStatus({ chain });
const { score } = useContext(ScorerContext);
const [confirmModalOpen, setConfirmModalOpen] = useState(false);

Expand Down
5 changes: 4 additions & 1 deletion app/hooks/useOnChainData.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export interface OnChainLastUpdates {
type SingleChainData = {
score: number;
providers: OnChainProviderType[];
expirationDate?: Date;
};

export interface OnChainScores {
Expand Down Expand Up @@ -67,6 +68,7 @@ const getOnChainDataForChain = async ({
const passportAttestationData = await getAttestationData(address!, chainId as keyof typeof onchainInfo);
let providers: OnChainProviderType[] = [];
let score = 0;
let expirationDate: Date | undefined;
if (passportAttestationData) {
const { onChainProviderInfo, hashes, issuanceDates, expirationDates } = await decodeProviderInformation(
passportAttestationData.passport
Expand All @@ -81,13 +83,14 @@ const getOnChainDataForChain = async ({
issuanceDate: new Date(issuanceDates[index].toNumber() * 1000),
}));

score = decodeScoreAttestation(passportAttestationData.score);
({ score, expirationDate } = decodeScoreAttestation(passportAttestationData.score));
}

return {
chainId,
providers,
score,
expirationDate,
};
};

Expand Down
21 changes: 14 additions & 7 deletions app/hooks/useOnChainStatus.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import { useContext, useEffect, useState } from "react";
import { CeramicContext } from "../context/ceramicContext";
import { ScorerContext } from "../context/scorerContext";
import { Chain, chains } from "../utils/chains";
import { AttestationProvider, OnChainState } from "../utils/AttestationProvider";
import { AttestationProvider } from "../utils/AttestationProvider";
import { OnChainStatus } from "../utils/onChainStatus";
import { useOnChainData } from "./useOnChainData";

export const useOnChainState = ({ chain }: { chain?: Chain }): OnChainState => {
export const useOnChainStatus = ({ chain }: { chain?: Chain }): OnChainStatus => {
const { allProvidersState } = useContext(CeramicContext);
const { data, isPending } = useOnChainData();
const { rawScore, scoreState } = useContext(ScorerContext);
Expand All @@ -16,20 +16,27 @@ export const useOnChainState = ({ chain }: { chain?: Chain }): OnChainState => {
const checkStatus = async () => {
if (!chain || isPending) return;

const { score, providers } = data[chain.id] || { score: 0, providers: [] };
const { score, providers, expirationDate } = data[chain.id] || { score: 0, providers: [] };

// Get the current active chain attestation provider
const attestationProvider: AttestationProvider | undefined = chains.find(
({ id }) => id === chain?.id
)?.attestationProvider;

const { status } = attestationProvider
? attestationProvider.checkOnChainState(allProvidersState, providers, rawScore, scoreState, score)
: { status: OnChainStatus.NOT_MOVED };
const status = attestationProvider
? attestationProvider.checkOnChainStatus(
allProvidersState,
providers,
rawScore,
scoreState,
score,
expirationDate
)
: OnChainStatus.NOT_MOVED;
setOnChainStatus(status);
};
checkStatus();
}, [allProvidersState, chain?.id, data, isPending, rawScore, scoreState]);

return { status: onChainStatus };
return onChainStatus;
};
113 changes: 49 additions & 64 deletions app/utils/AttestationProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,6 @@ type VeraxAndEASConfig = BaseProviderConfig & {

export type AttestationProviderConfig = EASConfig | VeraxAndEASConfig;

export type OnChainState = {
status: OnChainStatus;
expirationDate?: Date;
};

export interface AttestationProvider {
name: string;
status: AttestationProviderStatus;
Expand All @@ -43,13 +38,14 @@ export interface AttestationProvider {
verifierAddress: () => string;
verifierAbi: () => any;
getMultiAttestationRequest: (payload: {}) => Promise<AxiosResponse<any, any>>;
checkOnChainState: (
checkOnChainStatus: (
allProvidersState: AllProvidersState,
onChainProviders: OnChainProviderType[],
rawScore: number,
scoreState: ScoreStateType,
onChainScore: number
) => OnChainState;
onChainScore: number,
expirationDate?: Date
) => OnChainStatus;
}

class BaseAttestationProvider implements AttestationProvider {
Expand Down Expand Up @@ -92,59 +88,48 @@ class BaseAttestationProvider implements AttestationProvider {
});
}

checkOnChainState(
checkOnChainStatus(
allProvidersState: AllProvidersState,
onChainProviders: OnChainProviderType[],
rawScore: number,
scoreState: ScoreStateType,
onChainScore: number
): OnChainState {
onChainScore: number,
expirationDate?: Date
): OnChainStatus {
// This is default implementation that will check for differences in
// the on-chain providers and on-chain score
let status = OnChainStatus.LOADING;
const expirationDate = new Date(); // TODO

if (scoreState === "DONE") {
if (onChainProviders.length === 0) {
status = OnChainStatus.NOT_MOVED;
} else {
if (expirationDate && new Date() > expirationDate) {
status = OnChainStatus.MOVED_EXPIRED;
} else if (rawScore !== onChainScore) {
status = OnChainStatus.MOVED_OUT_OF_DATE;
} else {
const verifiedDbProviders: ProviderWithStamp[] = Object.values(allProvidersState).filter(
(provider): provider is ProviderWithStamp => provider.stamp !== undefined
);

const [equivalentProviders, differentProviders] = verifiedDbProviders.reduce(
([eq, diff], provider): [ProviderWithStamp[], ProviderWithStamp[]] => {
const expirationDateSeconds = Math.floor(
new Date(provider.stamp.credential.expirationDate).valueOf() / 1000
);
const issuanceDateSeconds = Math.floor(new Date(provider.stamp.credential.issuanceDate).valueOf() / 1000);

const isEquivalent = onChainProviders.some(
(onChainProvider) =>
onChainProvider.providerName === provider.stamp.provider &&
onChainProvider.credentialHash === provider.stamp.credential.credentialSubject?.hash &&
Math.floor(onChainProvider.expirationDate.valueOf() / 1000) === expirationDateSeconds &&
Math.floor(onChainProvider.issuanceDate.valueOf() / 1000) === issuanceDateSeconds
);
return isEquivalent ? [[...eq, provider], diff] : [eq, [...diff, provider]];
},
[[], []] as [ProviderWithStamp[], ProviderWithStamp[]]
);

status =
equivalentProviders.length === onChainProviders.length && differentProviders.length === 0
? OnChainStatus.MOVED_UP_TO_DATE
: OnChainStatus.MOVED_OUT_OF_DATE;
}
}
}
if (scoreState !== "DONE") return OnChainStatus.LOADING;

if (onChainProviders.length === 0) return OnChainStatus.NOT_MOVED;

if (expirationDate && new Date() > expirationDate) return OnChainStatus.MOVED_EXPIRED;

if (rawScore !== onChainScore) return OnChainStatus.MOVED_OUT_OF_DATE;

return { status, expirationDate };
const verifiedDbProviders: ProviderWithStamp[] = Object.values(allProvidersState).filter(
(provider): provider is ProviderWithStamp => provider.stamp !== undefined
);

const [equivalentProviders, differentProviders] = verifiedDbProviders.reduce(
([eq, diff], provider): [ProviderWithStamp[], ProviderWithStamp[]] => {
const expirationDateSeconds = Math.floor(new Date(provider.stamp.credential.expirationDate).valueOf() / 1000);
const issuanceDateSeconds = Math.floor(new Date(provider.stamp.credential.issuanceDate).valueOf() / 1000);

const isEquivalent = onChainProviders.some(
(onChainProvider) =>
onChainProvider.providerName === provider.stamp.provider &&
onChainProvider.credentialHash === provider.stamp.credential.credentialSubject?.hash &&
Math.floor(onChainProvider.expirationDate.valueOf() / 1000) === expirationDateSeconds &&
Math.floor(onChainProvider.issuanceDate.valueOf() / 1000) === issuanceDateSeconds
);
return isEquivalent ? [[...eq, provider], diff] : [eq, [...diff, provider]];
},
[[], []] as [ProviderWithStamp[], ProviderWithStamp[]]
);

return equivalentProviders.length === onChainProviders.length && differentProviders.length === 0
? OnChainStatus.MOVED_UP_TO_DATE
: OnChainStatus.MOVED_OUT_OF_DATE;
}
}

Expand Down Expand Up @@ -188,22 +173,22 @@ export class VeraxAndEASAttestationProvider extends EASAttestationProvider {
return this.easScanUrl;
}

checkOnChainState(
checkOnChainStatus(
allProvidersState: AllProvidersState,
onChainProviders: OnChainProviderType[],
rawScore: number,
scoreState: ScoreStateType,
onChainScore: number
): OnChainState {
let status = OnChainStatus.LOADING;
let expirationDate = undefined; // TODO

onChainScore: number,
expirationDate?: Date
): OnChainStatus {
// This is specific implementation for Verax where we only check for the score to be different
if (scoreState === "DONE" && onChainScore !== undefined) {
if (Number.isNaN(onChainScore)) status = OnChainStatus.NOT_MOVED;
else status = rawScore !== onChainScore ? OnChainStatus.MOVED_OUT_OF_DATE : OnChainStatus.MOVED_UP_TO_DATE;
if (scoreState !== "DONE" || onChainScore === undefined) {
return OnChainStatus.LOADING;
}

return { status, expirationDate };
if (expirationDate && new Date() > expirationDate) return OnChainStatus.MOVED_EXPIRED;

if (Number.isNaN(onChainScore)) return OnChainStatus.NOT_MOVED;
return rawScore !== onChainScore ? OnChainStatus.MOVED_OUT_OF_DATE : OnChainStatus.MOVED_UP_TO_DATE;
}
}
22 changes: 19 additions & 3 deletions app/utils/onChainStamps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ export type AttestationData = {
score: Attestation;
};

type DecodedScoreAttestation = {
score: number;
issuanceDate?: Date;
expirationDate?: Date;
};

const SCORE_MAX_AGE_MILLISECONDS = 1000 * 60 * 60 * 24 * 90; // 90 days

export async function getAttestationData(
address: string,
chainId: keyof typeof onchainInfo
Expand Down Expand Up @@ -109,9 +117,11 @@ export async function decodeProviderInformation(attestation: Attestation): Promi
return { onChainProviderInfo, hashes, issuanceDates, expirationDates };
}

export function decodeScoreAttestation(attestation: Attestation): number {
export function decodeScoreAttestation(attestation: Attestation): DecodedScoreAttestation {
if (attestation.data === "0x") {
return NaN;
return {
score: NaN,
};
}

const schemaEncoder = new SchemaEncoder("uint256 score,uint32 scorer_id,uint8 score_decimals");
Expand All @@ -121,6 +131,12 @@ export function decodeScoreAttestation(attestation: Attestation): number {
const score_decimals = decodedData.find(({ name }) => name === "score_decimals")?.value.value as number;

const score = parseFloat(formatUnits(score_as_integer, score_decimals));
const issuanceDate = new Date(BigNumber.from(attestation.time).mul(1000).toNumber()) || undefined;
const expirationDate = issuanceDate ? new Date(issuanceDate.getTime() + SCORE_MAX_AGE_MILLISECONDS) : undefined;

return score;
return {
score,
issuanceDate,
expirationDate,
};
}
Loading

0 comments on commit bec0c09

Please sign in to comment.