issue-166-open-timestamps #220
@ -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<SignedEvent | null> => {
|
||||
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)
|
||||
|
@ -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 {
|
||||
|
9
src/types/opentimestamps.d.ts
vendored
9
src/types/opentimestamps.d.ts
vendored
@ -10,7 +10,10 @@ interface OpenTimestamps {
|
||||
stamp(file: any): Promise<void>
|
||||
|
||||
// Verify the provided timestamp proof file
|
||||
verify(ots: string, file: string): Promise<void>
|
||||
verify(
|
||||
ots: string,
|
||||
file: string
|
||||
): Promise<TimestampVerficiationResponse | Record<string, never>>
|
||||
|
||||
// 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<boolean>
|
||||
}
|
||||
|
||||
interface TimestampVerficiationResponse {
|
||||
bitcoin: { timestamp: number; height: number }
|
||||
}
|
||||
|
@ -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<TimestampUpgradeVerifyResponse> => {
|
||||
const upgradedResult = await retry(() => upgrade(timestamp))
|
||||
const verifiedResult = await verify(upgradedResult)
|
||||
|
||||
console.log('verifiedResult: ', verifiedResult)
|
||||
|
||||
return verifiedResult
|
||||
}
|
||||
|
||||
export const upgradeTimestamps = async (
|
||||
timestamps: Timestamp[]
|
||||
): Promise<TimestampUpgradeResponse[]> => {
|
||||
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<TimestampUpgradeVerifyResponse> => {
|
||||
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<TimestampUpgradeVerifyResponse> => {
|
||||
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<string> => {
|
||||
const detachedTimestamp =
|
||||
window.OpenTimestamps.DetachedTimestampFile.fromBytes(
|
||||
|
Loading…
Reference in New Issue
Block a user