diff --git a/src/pages/verify/index.tsx b/src/pages/verify/index.tsx index de66991..f470b49 100644 --- a/src/pages/verify/index.tsx +++ b/src/pages/verify/index.tsx @@ -5,7 +5,13 @@ import { useEffect, useRef, useState } from 'react' import { toast } from 'react-toastify' import { LoadingSpinner } from '../../components/LoadingSpinner' import { NostrController } from '../../controllers' -import { DocSignatureEvent, Meta } from '../../types' +import { + DocSignatureEvent, + Meta, + SignedEvent, + Timestamp, + TimestampUpgradeVerifyResponse +} from '../../types' import { decryptArrayBuffer, extractMarksFromSignedMeta, @@ -15,7 +21,10 @@ import { parseJson, readContentOfZipEntry, signEventForMetaFile, - getCurrentUserFiles + getCurrentUserFiles, + updateUsersAppData, + npubToHex, + sendNotification } from '../../utils' import styles from './style.module.scss' import { useLocation } from 'react-router-dom' @@ -48,6 +57,12 @@ import { faFile, faFileDownload } from '@fortawesome/free-solid-svg-icons' +import { + upgradeAndVerifyTimestamp, + upgradeTimestamps, + verifyTimestamps +} from '../../utils/opentimestamps.ts' +import _ from 'lodash' interface PdfViewProps { files: CurrentUserFile[] @@ -180,7 +195,8 @@ export const VerifyPage = () => { signers, viewers, fileHashes, - parsedSignatureEvents + parsedSignatureEvents, + timestamps } = useSigitMeta(meta) const [files, setFiles] = useState<{ [filename: string]: SigitFile }>({}) @@ -190,6 +206,16 @@ export const VerifyPage = () => { [key: string]: string | null }>({}) + const signTimestampEvent = async (signerContent: { + timestamps: Timestamp[] + }): Promise => { + return await signEventForMetaFile( + JSON.stringify(signerContent), + nostrController, + setIsLoading + ) + } + useEffect(() => { if (Object.entries(files).length > 0) { const tmp = getCurrentUserFiles(files, currentFileHashes, fileHashes) @@ -198,6 +224,89 @@ export const VerifyPage = () => { }, [currentFileHashes, fileHashes, files]) useEffect(() => { + if (timestamps && timestamps.length > 0) { + if (timestamps.every((t) => !!t.verification)) { + return + } + const upgradeT = async (timestamps: Timestamp[]) => { + const verifiedResults = await Promise.all( + timestamps.map(upgradeAndVerifyTimestamp) + ) + + const upgradedTimestamps = verifiedResults + .filter((t) => t.upgraded || isNewlyVerified(t, timestamps)) + .map((t) => { + const timestamp = t.value + if (t.verified) { + timestamp.verification = t.verification + } + return timestamp + }) + + const isNewlyVerified = ( + upgradedTimestamp: TimestampUpgradeVerifyResponse, + timestamps: Timestamp[] + ) => { + if (!upgradedTimestamp.verified) return false + const oldT = timestamps.find( + (t) => t.nostrId === upgradedTimestamp.value.nostrId + ) + if (!oldT) return false + if (!oldT.verification && upgradedTimestamp.verified) return true + } + + const signedEvent = await signTimestampEvent({ + timestamps: upgradedTimestamps + }) + if (!signedEvent) return + + const finalTimestamps = timestamps.map((t) => { + const upgraded = upgradedTimestamps.find( + (tu) => tu.nostrId === t.nostrId + ) + if (upgraded) { + return { + ...upgraded, + signature: JSON.stringify(signedEvent, null, 2) + } + } + return t + }) + + const updatedMeta = _.cloneDeep(meta) + updatedMeta.timestamps = [...finalTimestamps] + updatedMeta.modifiedAt = unixNow() + + const updatedEvent = await updateUsersAppData(updatedMeta) + if (!updatedEvent) return + + const userSet = new Set<`npub1${string}`>() + signers.forEach((signer) => { + if (signer !== usersPubkey) { + userSet.add(signer) + } + }) + + viewers.forEach((viewer) => { + userSet.add(viewer) + }) + + const users = Array.from(userSet) + const promises = users.map((user) => + sendNotification(npubToHex(user)!, updatedMeta) + ) + + await Promise.all(promises) + + setTimestamps(finalTimestamps) + setMeta(meta) + } + upgradeT(timestamps) + } + }, [timestamps]) + + useEffect(() => { + console.log('this runs') if (metaInNavState && encryptionKey) { const processSigit = async () => { setIsLoading(true) diff --git a/src/types/core.ts b/src/types/core.ts index ea12271..0216f2a 100644 --- a/src/types/core.ts +++ b/src/types/core.ts @@ -43,14 +43,20 @@ export interface Sigit { export interface Timestamp { nostrId: string timestamp: string - isComplete?: boolean + verification?: TimestampVerification + signature?: string } -export interface TimestampUpgradeResponse { +export interface TimestampVerification { + height: number +} + +export interface TimestampUpgradeVerifyResponse { + value: Timestamp upgraded: boolean - nostrId: string - timestamp: string - isComplete: boolean + isComplete?: boolean + verified?: boolean + verification?: TimestampVerification } export interface UserAppData { diff --git a/src/types/opentimestamps.d.ts b/src/types/opentimestamps.d.ts index f3398d2..825268d 100644 --- a/src/types/opentimestamps.d.ts +++ b/src/types/opentimestamps.d.ts @@ -10,7 +10,10 @@ interface OpenTimestamps { stamp(file: any): Promise // Verify the provided timestamp proof file - verify(ots: string, file: string): Promise + verify( + ots: string, + file: string + ): Promise> // Other utilities or operations (like OpSHA256, serialization) Ops: { @@ -28,3 +31,7 @@ interface OpenTimestamps { // Other potential methods based on repo functions upgrade(file: any): Promise } + +interface TimestampVerficiationResponse { + bitcoin: { timestamp: number; height: number } +} diff --git a/src/utils/opentimestamps.ts b/src/utils/opentimestamps.ts index 4cbc036..2af0148 100644 --- a/src/utils/opentimestamps.ts +++ b/src/utils/opentimestamps.ts @@ -1,7 +1,13 @@ -import { Timestamp } from '../types' -import { retry } from './retry.ts' +import { + Timestamp, + TimestampUpgradeResponse, + TimestampUpgradeVerifyResponse +} from '../types' +import { retry, retryAll } from './retry.ts' import { bytesToHex } from '@noble/hashes/utils' import { utf8Encoder } from 'nostr-tools/utils' +import { hexStringToUint8Array } from './string.ts' +import { isPromiseFulfilled } from './utils.ts' /** * Generates a timestamp for the provided nostr event ID. @@ -21,6 +27,87 @@ export const generateTimestamp = async ( } } +export const upgradeAndVerifyTimestamp = async ( + timestamp: Timestamp +): Promise => { + const upgradedResult = await retry(() => upgrade(timestamp)) + const verifiedResult = await verify(upgradedResult) + + console.log('verifiedResult: ', verifiedResult) + + return verifiedResult +} + +export const upgradeTimestamps = async ( + timestamps: Timestamp[] +): Promise => { + const resolved = await retryAll(timestamps.map((t) => () => upgrade(t))) + return resolved.filter(isPromiseFulfilled).map((res) => res.value) +} + +export const verifyTimestamps = async (timestamps: Timestamp[]) => { + const settledResults = await Promise.allSettled( + timestamps.map(async (timestamp) => verify(timestamp)) + ) + return settledResults.filter(isPromiseFulfilled).map((res) => res.value) +} + +const upgrade = async ( + t: Timestamp +): Promise => { + let detachedTimestamp = + window.OpenTimestamps.DetachedTimestampFile.deserialize( + hexStringToUint8Array(t.timestamp) + ) + + const changed: boolean = + await window.OpenTimestamps.upgrade(detachedTimestamp) + if (changed) { + const updated = detachedTimestamp.serializeToBytes() + + const value = { + ...t, + timestamp: bytesToHex(updated) + } + + return { + value, + upgraded: true, + isComplete: detachedTimestamp.timestamp.isTimestampComplete() + } + } + return { + value: t, + upgraded: false, + isComplete: detachedTimestamp.timestamp.isTimestampComplete() + } +} + +export const verify = async ( + t: TimestampUpgradeVerifyResponse +): Promise => { + const detachedNostrId = window.OpenTimestamps.DetachedTimestampFile.fromBytes( + new window.OpenTimestamps.Ops.OpSHA256(), + utf8Encoder.encode(t.value.nostrId) + ) + + let detachedTimestamp = + window.OpenTimestamps.DetachedTimestampFile.deserialize( + hexStringToUint8Array(t.value.timestamp) + ) + + const res = await window.OpenTimestamps.verify( + detachedTimestamp, + detachedNostrId + ) + + return { + ...t, + verified: !!res.bitcoin, + verification: res?.bitcoin || null + } +} + const timestamp = async (nostrId: string): Promise => { const detachedTimestamp = window.OpenTimestamps.DetachedTimestampFile.fromBytes(