issue-166-open-timestamps #220
@ -6,7 +6,7 @@ module.exports = {
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:react-hooks/recommended'
|
||||
],
|
||||
ignorePatterns: ['dist', '.eslintrc.cjs', 'licenseChecker.cjs'],
|
||||
ignorePatterns: ['dist', '.eslintrc.cjs', 'licenseChecker.cjs', "*.min.js"],
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['react-refresh'],
|
||||
rules: {
|
||||
|
@ -8,6 +8,7 @@
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script src="/opentimestamps.min.js"></script>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "sigit",
|
||||
"version": "0.0.0",
|
||||
"version": "0.0.0-beta",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "sigit",
|
||||
"version": "0.0.0",
|
||||
"version": "0.0.0-beta",
|
||||
"hasInstallScript": true,
|
||||
"license": "AGPL-3.0-or-later ",
|
||||
"dependencies": {
|
||||
|
2
public/opentimestamps.min.js
vendored
Normal file
2
public/opentimestamps.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -4,6 +4,7 @@ import {
|
||||
fromUnixTimestamp,
|
||||
hexToNpub,
|
||||
npubToHex,
|
||||
SigitStatus,
|
||||
SignStatus
|
||||
} from '../../utils'
|
||||
import { useSigitMeta } from '../../hooks/useSigitMeta'
|
||||
@ -15,6 +16,8 @@ import {
|
||||
faCalendar,
|
||||
faCalendarCheck,
|
||||
faCalendarPlus,
|
||||
faCheck,
|
||||
faClock,
|
||||
faEye,
|
||||
faFile,
|
||||
faFileCircleExclamation
|
||||
@ -22,7 +25,7 @@ import {
|
||||
import { getExtensionIconLabel } from '../getExtensionIconLabel'
|
||||
import { useAppSelector } from '../../hooks/store'
|
||||
import { DisplaySigner } from '../DisplaySigner'
|
||||
import { Meta } from '../../types'
|
||||
import { Meta, OpenTimestamp } from '../../types'
|
||||
import { extractFileExtensions } from '../../utils/file'
|
||||
import { UserAvatar } from '../UserAvatar'
|
||||
|
||||
@ -42,7 +45,9 @@ export const UsersDetails = ({ meta }: UsersDetailsProps) => {
|
||||
completedAt,
|
||||
parsedSignatureEvents,
|
||||
signedStatus,
|
||||
isValid
|
||||
isValid,
|
||||
id,
|
||||
timestamps
|
||||
} = useSigitMeta(meta)
|
||||
const { usersPubkey } = useAppSelector((state) => state.auth)
|
||||
const userCanSign =
|
||||
@ -51,6 +56,50 @@ export const UsersDetails = ({ meta }: UsersDetailsProps) => {
|
||||
|
||||
const { extensions, isSame } = extractFileExtensions(Object.keys(fileHashes))
|
||||
|
||||
const isTimestampVerified = (
|
||||
timestamps: OpenTimestamp[],
|
||||
nostrId: string
|
||||
): boolean => {
|
||||
const matched = timestamps.find((t) => t.nostrId === nostrId)
|
||||
return !!(matched && matched.verification)
|
||||
}
|
||||
|
||||
const getOpenTimestampsInfo = (
|
||||
timestamps: OpenTimestamp[],
|
||||
nostrId: string
|
||||
) => {
|
||||
if (isTimestampVerified(timestamps, nostrId)) {
|
||||
return <FontAwesomeIcon className={styles.ticket} icon={faCheck} />
|
||||
eugene marked this conversation as resolved
Outdated
|
||||
} else {
|
||||
return <FontAwesomeIcon className={styles.ticket} icon={faClock} />
|
||||
}
|
||||
}
|
||||
|
||||
const getCompletedOpenTimestampsInfo = (timestamp: OpenTimestamp) => {
|
||||
if (timestamp.verification) {
|
||||
return <FontAwesomeIcon className={styles.ticket} icon={faCheck} />
|
||||
} else {
|
||||
return <FontAwesomeIcon className={styles.ticket} icon={faClock} />
|
||||
}
|
||||
}
|
||||
|
||||
const getTimestampTooltipTitle = (label: string, isVerified: boolean) => {
|
||||
return `${label} / Open Timestamp ${isVerified ? 'Verified' : 'Pending'}`
|
||||
}
|
||||
|
||||
const isUserSignatureTimestampVerified = () => {
|
||||
if (
|
||||
userCanSign &&
|
||||
hexToNpub(usersPubkey) in parsedSignatureEvents &&
|
||||
timestamps &&
|
||||
timestamps.length > 0
|
||||
) {
|
||||
const nostrId = parsedSignatureEvents[hexToNpub(usersPubkey)].id
|
||||
return isTimestampVerified(timestamps, nostrId)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
return submittedBy ? (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.section}>
|
||||
@ -115,19 +164,35 @@ export const UsersDetails = ({ meta }: UsersDetailsProps) => {
|
||||
<p>Details</p>
|
||||
|
||||
<Tooltip
|
||||
title={'Publication date'}
|
||||
title={getTimestampTooltipTitle(
|
||||
'Publication date',
|
||||
!!(timestamps && id && isTimestampVerified(timestamps, id))
|
||||
)}
|
||||
placement="top"
|
||||
arrow
|
||||
disableInteractive
|
||||
>
|
||||
<span className={styles.detailsItem}>
|
||||
<FontAwesomeIcon icon={faCalendarPlus} />{' '}
|
||||
{createdAt ? formatTimestamp(createdAt) : <>—</>}
|
||||
{createdAt ? formatTimestamp(createdAt) : <>—</>}{' '}
|
||||
{timestamps &&
|
||||
timestamps.length > 0 &&
|
||||
id &&
|
||||
getOpenTimestampsInfo(timestamps, id)}
|
||||
</span>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip
|
||||
title={'Completion date'}
|
||||
title={getTimestampTooltipTitle(
|
||||
'Completion date',
|
||||
!!(
|
||||
signedStatus === SigitStatus.Complete &&
|
||||
completedAt &&
|
||||
timestamps &&
|
||||
timestamps.length > 0 &&
|
||||
timestamps[timestamps.length - 1].verification
|
||||
)
|
||||
)}
|
||||
placement="top"
|
||||
arrow
|
||||
disableInteractive
|
||||
@ -135,13 +200,26 @@ export const UsersDetails = ({ meta }: UsersDetailsProps) => {
|
||||
<span className={styles.detailsItem}>
|
||||
<FontAwesomeIcon icon={faCalendarCheck} />{' '}
|
||||
{completedAt ? formatTimestamp(completedAt) : <>—</>}
|
||||
{signedStatus === SigitStatus.Complete &&
|
||||
completedAt &&
|
||||
timestamps &&
|
||||
timestamps.length > 0 && (
|
||||
<span className={styles.ticket}>
|
||||
{getCompletedOpenTimestampsInfo(
|
||||
timestamps[timestamps.length - 1]
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</Tooltip>
|
||||
|
||||
{/* User signed date */}
|
||||
{userCanSign ? (
|
||||
<Tooltip
|
||||
title={'Your signature date'}
|
||||
title={getTimestampTooltipTitle(
|
||||
'Your signature date',
|
||||
isUserSignatureTimestampVerified()
|
||||
)}
|
||||
placement="top"
|
||||
arrow
|
||||
disableInteractive
|
||||
@ -161,6 +239,16 @@ export const UsersDetails = ({ meta }: UsersDetailsProps) => {
|
||||
) : (
|
||||
<>—</>
|
||||
)}
|
||||
{hexToNpub(usersPubkey) in parsedSignatureEvents &&
|
||||
timestamps &&
|
||||
timestamps.length > 0 && (
|
||||
<span className={styles.ticket}>
|
||||
{getOpenTimestampsInfo(
|
||||
timestamps,
|
||||
parsedSignatureEvents[hexToNpub(usersPubkey)].id
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
|
@ -31,8 +31,6 @@
|
||||
padding: 5px;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: start;
|
||||
|
||||
> :first-child {
|
||||
padding: 5px;
|
||||
@ -44,3 +42,7 @@
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.ticket {
|
||||
margin-left: auto;
|
||||
}
|
||||
eugene marked this conversation as resolved
enes
commented
Tiny styling improvement, 5px padding to match the first icon. Tiny styling improvement, 5px padding to match the first icon.
|
||||
|
@ -3,7 +3,8 @@ import {
|
||||
CreateSignatureEventContent,
|
||||
DocSignatureEvent,
|
||||
Meta,
|
||||
SignedEventContent
|
||||
SignedEventContent,
|
||||
OpenTimestamp
|
||||
} from '../types'
|
||||
import { Mark } from '../types/mark'
|
||||
import {
|
||||
@ -58,6 +59,8 @@ export interface FlatMeta
|
||||
signersStatus: {
|
||||
[signer: `npub1${string}`]: SignStatus
|
||||
}
|
||||
|
||||
timestamps?: OpenTimestamp[]
|
||||
}
|
||||
|
||||
/**
|
||||
@ -162,7 +165,6 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
|
||||
setEncryptionKey(decrypted)
|
||||
}
|
||||
}
|
||||
|
||||
// Temp. map to hold events and signers
|
||||
const parsedSignatureEventsMap = new Map<
|
||||
`npub1${string}`,
|
||||
@ -276,6 +278,7 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
|
||||
createSignature: meta?.createSignature,
|
||||
docSignatures: meta?.docSignatures,
|
||||
keys: meta?.keys,
|
||||
timestamps: meta?.timestamps,
|
||||
isValid,
|
||||
kind,
|
||||
tags,
|
||||
|
@ -19,6 +19,7 @@ import {
|
||||
CreateSignatureEventContent,
|
||||
Meta,
|
||||
ProfileMetadata,
|
||||
SignedEvent,
|
||||
User,
|
||||
UserRole
|
||||
} from '../../types'
|
||||
@ -62,6 +63,7 @@ import {
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { getSigitFile, SigitFile } from '../../utils/file.ts'
|
||||
import _ from 'lodash'
|
||||
import { generateTimestamp } from '../../utils/opentimestamps.ts'
|
||||
|
||||
export const CreatePage = () => {
|
||||
const navigate = useNavigate()
|
||||
@ -642,6 +644,11 @@ export const CreatePage = () => {
|
||||
return receivers.map((receiver) => sendNotification(receiver, meta))
|
||||
}
|
||||
|
||||
const extractNostrId = (stringifiedEvent: string): string => {
|
||||
const e = JSON.parse(stringifiedEvent) as SignedEvent
|
||||
return e.id
|
||||
}
|
||||
|
||||
const handleCreate = async () => {
|
||||
try {
|
||||
if (!validateInputs()) return
|
||||
@ -691,6 +698,12 @@ export const CreatePage = () => {
|
||||
const keys = await generateKeys(pubkeys, encryptionKey)
|
||||
if (!keys) return
|
||||
|
||||
setLoadingSpinnerDesc('Generating an open timestamp.')
|
||||
|
||||
const timestamp = await generateTimestamp(
|
||||
extractNostrId(createSignature)
|
||||
)
|
||||
|
||||
const meta: Meta = {
|
||||
createSignature,
|
||||
keys,
|
||||
@ -698,6 +711,10 @@ export const CreatePage = () => {
|
||||
docSignatures: {}
|
||||
}
|
||||
|
||||
if (timestamp) {
|
||||
meta.timestamps = [timestamp]
|
||||
}
|
||||
|
||||
setLoadingSpinnerDesc('Updating user app data')
|
||||
const event = await updateUsersAppData(meta)
|
||||
if (!event) return
|
||||
|
@ -53,6 +53,7 @@ import {
|
||||
SigitFile
|
||||
} from '../../utils/file.ts'
|
||||
import { ARRAY_BUFFER, DEFLATE } from '../../utils/const.ts'
|
||||
import { generateTimestamp } from '../../utils/opentimestamps.ts'
|
||||
|
||||
enum SignedStatus {
|
||||
Fully_Signed,
|
||||
@ -566,6 +567,14 @@ export const SignPage = () => {
|
||||
|
||||
const updatedMeta = updateMetaSignatures(meta, signedEvent)
|
||||
|
||||
setLoadingSpinnerDesc('Generating an open timestamp.')
|
||||
|
||||
const timestamp = await generateTimestamp(signedEvent.id)
|
||||
if (timestamp) {
|
||||
updatedMeta.timestamps = [...(updatedMeta.timestamps || []), timestamp]
|
||||
updatedMeta.modifiedAt = unixNow()
|
||||
}
|
||||
|
||||
if (await isOnline()) {
|
||||
await handleOnlineFlow(updatedMeta)
|
||||
} else {
|
||||
|
@ -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,
|
||||
OpenTimestamp,
|
||||
OpenTimestampUpgradeVerifyResponse
|
||||
} from '../../types'
|
||||
import {
|
||||
decryptArrayBuffer,
|
||||
getHash,
|
||||
@ -14,7 +20,10 @@ import {
|
||||
parseJson,
|
||||
readContentOfZipEntry,
|
||||
signEventForMetaFile,
|
||||
getCurrentUserFiles
|
||||
getCurrentUserFiles,
|
||||
updateUsersAppData,
|
||||
npubToHex,
|
||||
sendNotification
|
||||
} from '../../utils'
|
||||
import styles from './style.module.scss'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
@ -26,7 +35,7 @@ import { saveAs } from 'file-saver'
|
||||
import { Container } from '../../components/Container'
|
||||
import { useSigitMeta } from '../../hooks/useSigitMeta.tsx'
|
||||
import { StickySideColumns } from '../../layouts/StickySideColumns.tsx'
|
||||
import { UsersDetails } from '../../components/UsersDetails.tsx/index.tsx'
|
||||
import { UsersDetails } from '../../components/UsersDetails.tsx'
|
||||
import FileList from '../../components/FileList'
|
||||
import { CurrentUserFile } from '../../types/file.ts'
|
||||
import { Mark } from '../../types/mark.ts'
|
||||
@ -44,6 +53,8 @@ import {
|
||||
faFile,
|
||||
faFileDownload
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { upgradeAndVerifyTimestamp } from '../../utils/opentimestamps.ts'
|
||||
import _ from 'lodash'
|
||||
|
||||
interface PdfViewProps {
|
||||
files: CurrentUserFile[]
|
||||
@ -176,7 +187,8 @@ export const VerifyPage = () => {
|
||||
signers,
|
||||
viewers,
|
||||
fileHashes,
|
||||
parsedSignatureEvents
|
||||
parsedSignatureEvents,
|
||||
timestamps
|
||||
} = useSigitMeta(meta)
|
||||
|
||||
const [files, setFiles] = useState<{ [filename: string]: SigitFile }>({})
|
||||
@ -186,6 +198,16 @@ export const VerifyPage = () => {
|
||||
[key: string]: string | null
|
||||
}>({})
|
||||
|
||||
const signTimestampEvent = async (signerContent: {
|
||||
timestamps: OpenTimestamp[]
|
||||
}): Promise<SignedEvent | null> => {
|
||||
return await signEventForMetaFile(
|
||||
JSON.stringify(signerContent),
|
||||
nostrController,
|
||||
setIsLoading
|
||||
)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (Object.entries(files).length > 0) {
|
||||
const tmp = getCurrentUserFiles(files, currentFileHashes, fileHashes)
|
||||
@ -193,6 +215,147 @@ export const VerifyPage = () => {
|
||||
}
|
||||
}, [currentFileHashes, fileHashes, files])
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
timestamps &&
|
||||
timestamps.length > 0 &&
|
||||
usersPubkey &&
|
||||
submittedBy &&
|
||||
parsedSignatureEvents
|
||||
) {
|
||||
if (timestamps.every((t) => !!t.verification)) {
|
||||
return
|
||||
}
|
||||
const upgradeT = async (timestamps: OpenTimestamp[]) => {
|
||||
try {
|
||||
setLoadingSpinnerDesc('Upgrading your timestamps.')
|
||||
|
||||
const findCreatorTimestamp = (timestamps: OpenTimestamp[]) => {
|
||||
if (usersPubkey === submittedBy) {
|
||||
return timestamps[0]
|
||||
}
|
||||
}
|
||||
|
||||
const findSignerTimestamp = (timestamps: OpenTimestamp[]) => {
|
||||
const parsedEvent = parsedSignatureEvents[hexToNpub(usersPubkey)]
|
||||
if (parsedEvent?.id) {
|
||||
return timestamps.find((t) => t.nostrId === parsedEvent.id)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if timestamp verification has been achieved for the first time.
|
||||
* Note that the upgrade flag is separate from verification. It is possible for a timestamp
|
||||
* to not be upgraded, but to be verified for the first time.
|
||||
* @param upgradedTimestamp
|
||||
* @param timestamps
|
||||
*/
|
||||
const isNewlyVerified = (
|
||||
upgradedTimestamp: OpenTimestampUpgradeVerifyResponse,
|
||||
timestamps: OpenTimestamp[]
|
||||
) => {
|
||||
if (!upgradedTimestamp.verified) return false
|
||||
const oldT = timestamps.find(
|
||||
(t) => t.nostrId === upgradedTimestamp.timestamp.nostrId
|
||||
)
|
||||
if (!oldT) return false
|
||||
if (!oldT.verification && upgradedTimestamp.verified) return true
|
||||
}
|
||||
|
||||
const userTimestamps: OpenTimestamp[] = []
|
||||
|
||||
const creatorTimestamp = findCreatorTimestamp(timestamps)
|
||||
if (creatorTimestamp) {
|
||||
userTimestamps.push(creatorTimestamp)
|
||||
}
|
||||
|
||||
const signerTimestamp = findSignerTimestamp(timestamps)
|
||||
if (signerTimestamp) {
|
||||
userTimestamps.push(signerTimestamp)
|
||||
}
|
||||
|
||||
if (userTimestamps.every((t) => !!t.verification)) {
|
||||
return
|
||||
}
|
||||
|
||||
const upgradedUserTimestamps = await Promise.all(
|
||||
userTimestamps.map(upgradeAndVerifyTimestamp)
|
||||
)
|
||||
|
||||
const upgradedTimestamps = upgradedUserTimestamps
|
||||
.filter((t) => t.upgraded || isNewlyVerified(t, userTimestamps))
|
||||
.map((t) => {
|
||||
const timestamp: OpenTimestamp = { ...t.timestamp }
|
||||
if (t.verified) {
|
||||
timestamp.verification = t.verification
|
||||
}
|
||||
return timestamp
|
||||
})
|
||||
|
||||
if (upgradedTimestamps.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
setLoadingSpinnerDesc('Signing a timestamp upgrade event.')
|
||||
|
||||
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)
|
||||
|
||||
toast.success('Timestamp updates have been sent successfully.')
|
||||
|
||||
setMeta(meta)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
toast.error(
|
||||
'There was an error upgrading or verifying your timestamps!'
|
||||
)
|
||||
}
|
||||
}
|
||||
upgradeT(timestamps)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [timestamps, submittedBy, parsedSignatureEvents])
|
||||
|
||||
useEffect(() => {
|
||||
if (metaInNavState && encryptionKey) {
|
||||
const processSigit = async () => {
|
||||
|
@ -18,6 +18,7 @@ export interface Meta {
|
||||
docSignatures: { [key: `npub1${string}`]: string }
|
||||
exportSignature?: string
|
||||
keys?: { sender: string; keys: { [user: `npub1${string}`]: string } }
|
||||
timestamps?: OpenTimestamp[]
|
||||
}
|
||||
|
||||
export interface CreateSignatureEventContent {
|
||||
@ -39,6 +40,25 @@ export interface Sigit {
|
||||
meta: Meta
|
||||
}
|
||||
|
||||
export interface OpenTimestamp {
|
||||
nostrId: string
|
||||
value: string
|
||||
verification?: OpenTimestampVerification
|
||||
signature?: string
|
||||
}
|
||||
|
||||
export interface OpenTimestampVerification {
|
||||
height: number
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
export interface OpenTimestampUpgradeVerifyResponse {
|
||||
timestamp: OpenTimestamp
|
||||
upgraded: boolean
|
||||
verified?: boolean
|
||||
verification?: OpenTimestampVerification
|
||||
}
|
||||
|
||||
export interface UserAppData {
|
||||
/**
|
||||
* Key will be id of create signature
|
||||
|
38
src/types/opentimestamps.d.ts
vendored
Normal file
38
src/types/opentimestamps.d.ts
vendored
Normal file
@ -0,0 +1,38 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
interface OpenTimestamps {
|
||||
// Create a detached timestamp file from a buffer or file hash
|
||||
DetachedTimestampFile: {
|
||||
fromHash(op: any, hash: Uint8Array): any
|
||||
fromBytes(op: any, buffer: Uint8Array): any
|
||||
deserialize(buffer: any): any
|
||||
}
|
||||
|
||||
// Stamp the provided timestamp file and return a Promise
|
||||
stamp(file: any): Promise<void>
|
||||
|
||||
// Verify the provided timestamp proof file
|
||||
verify(
|
||||
ots: string,
|
||||
file: string
|
||||
): Promise<TimestampVerficiationResponse | Record<string, never>>
|
||||
|
||||
// Other utilities or operations (like OpSHA256, serialization)
|
||||
Ops: {
|
||||
OpSHA256: any
|
||||
OpSHA1?: any
|
||||
}
|
||||
|
||||
Context: {
|
||||
StreamSerialization: any
|
||||
}
|
||||
|
||||
// Load a timestamp file from a buffer
|
||||
deserialize(bytes: Uint8Array): any
|
||||
|
||||
// Other potential methods based on repo functions
|
||||
upgrade(file: any): Promise<boolean>
|
||||
}
|
||||
|
||||
interface TimestampVerficiationResponse {
|
||||
bitcoin: { timestamp: number; height: number }
|
||||
}
|
1
src/types/system/index.d.ts
vendored
1
src/types/system/index.d.ts
vendored
@ -3,5 +3,6 @@ import type { WindowNostr } from 'nostr-tools/nip07'
|
||||
declare global {
|
||||
interface Window {
|
||||
nostr?: WindowNostr
|
||||
OpenTimestamps: OpenTimestamps
|
||||
}
|
||||
}
|
||||
|
119
src/utils/opentimestamps.ts
Normal file
119
src/utils/opentimestamps.ts
Normal file
@ -0,0 +1,119 @@
|
||||
import { OpenTimestamp, OpenTimestampUpgradeVerifyResponse } from '../types'
|
||||
import { retry } from './retry.ts'
|
||||
import { bytesToHex } from '@noble/hashes/utils'
|
||||
import { utf8Encoder } from 'nostr-tools/utils'
|
||||
import { hexStringToUint8Array } from './string.ts'
|
||||
|
||||
/**
|
||||
* Generates a timestamp for the provided nostr event ID.
|
||||
* @returns Timestamp with its value and the nostr event ID.
|
||||
*/
|
||||
export const generateTimestamp = async (
|
||||
nostrId: string
|
||||
): Promise<OpenTimestamp | undefined> => {
|
||||
try {
|
||||
return {
|
||||
value: await retry(() => timestamp(nostrId)),
|
||||
nostrId: nostrId
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to upgrade (i.e. add Bitcoin blockchain attestations) and verify the provided timestamp.
|
||||
* Returns the same timestamp, alongside additional information required to decide if any further
|
||||
* timestamp updates are required.
|
||||
* @param timestamp
|
||||
*/
|
||||
export const upgradeAndVerifyTimestamp = async (
|
||||
timestamp: OpenTimestamp
|
||||
): Promise<OpenTimestampUpgradeVerifyResponse> => {
|
||||
const upgradedResult = await upgrade(timestamp)
|
||||
return await verify(upgradedResult)
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to upgrade a timestamp. If an upgrade is available,
|
||||
* it will add new data to detachedTimestamp.
|
||||
* The upgraded flag indicates if an upgrade has been performed.
|
||||
* @param t - timestamp
|
||||
*/
|
||||
export const upgrade = async (
|
||||
t: OpenTimestamp
|
||||
): Promise<OpenTimestampUpgradeVerifyResponse> => {
|
||||
const detachedTimestamp =
|
||||
window.OpenTimestamps.DetachedTimestampFile.deserialize(
|
||||
hexStringToUint8Array(t.value)
|
||||
)
|
||||
|
||||
const changed: boolean =
|
||||
await window.OpenTimestamps.upgrade(detachedTimestamp)
|
||||
if (changed) {
|
||||
const updated = detachedTimestamp.serializeToBytes()
|
||||
|
||||
const value = {
|
||||
...t,
|
||||
timestamp: bytesToHex(updated)
|
||||
}
|
||||
|
||||
return {
|
||||
timestamp: value,
|
||||
upgraded: true
|
||||
}
|
||||
}
|
||||
return {
|
||||
timestamp: t,
|
||||
upgraded: false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to verify a timestamp. If verification is available,
|
||||
* it will be included in the returned object.
|
||||
* @param t - timestamp
|
||||
*/
|
||||
export const verify = async (
|
||||
t: OpenTimestampUpgradeVerifyResponse
|
||||
): Promise<OpenTimestampUpgradeVerifyResponse> => {
|
||||
const detachedNostrId = window.OpenTimestamps.DetachedTimestampFile.fromBytes(
|
||||
new window.OpenTimestamps.Ops.OpSHA256(),
|
||||
utf8Encoder.encode(t.timestamp.nostrId)
|
||||
)
|
||||
|
||||
const detachedTimestamp =
|
||||
window.OpenTimestamps.DetachedTimestampFile.deserialize(
|
||||
hexStringToUint8Array(t.timestamp.value)
|
||||
)
|
||||
|
||||
const res = await window.OpenTimestamps.verify(
|
||||
detachedTimestamp,
|
||||
detachedNostrId
|
||||
)
|
||||
|
||||
return {
|
||||
...t,
|
||||
verified: !!res.bitcoin,
|
||||
verification: res?.bitcoin || null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Timestamps a nostrId.
|
||||
* @param nostrId
|
||||
*/
|
||||
const timestamp = async (nostrId: string): Promise<string> => {
|
||||
const detachedTimestamp =
|
||||
window.OpenTimestamps.DetachedTimestampFile.fromBytes(
|
||||
new window.OpenTimestamps.Ops.OpSHA256(),
|
||||
utf8Encoder.encode(nostrId)
|
||||
)
|
||||
|
||||
await window.OpenTimestamps.stamp(detachedTimestamp)
|
||||
const ctx = new window.OpenTimestamps.Context.StreamSerialization()
|
||||
detachedTimestamp.serialize(ctx)
|
||||
const timestampBytes = ctx.getOutput()
|
||||
return bytesToHex(timestampBytes)
|
||||
}
|
25
src/utils/retry.ts
Normal file
25
src/utils/retry.ts
Normal file
@ -0,0 +1,25 @@
|
||||
export const retryAll = async <T>(
|
||||
promises: (() => Promise<T>)[],
|
||||
retries: number = 3,
|
||||
delay: number = 1000
|
||||
) => {
|
||||
const wrappedPromises = promises.map((fn) => retry(fn, retries, delay))
|
||||
return Promise.allSettled(wrappedPromises)
|
||||
}
|
||||
|
||||
export const retry = async <T>(
|
||||
fn: () => Promise<T>,
|
||||
retries: number = 3,
|
||||
delay: number = 1000
|
||||
): Promise<T> => {
|
||||
try {
|
||||
return await fn()
|
||||
} catch (err) {
|
||||
if (retries === 0) {
|
||||
return Promise.reject(err)
|
||||
}
|
||||
return new Promise((resolve) =>
|
||||
setTimeout(() => resolve(retry(fn, retries - 1)), delay)
|
||||
)
|
||||
}
|
||||
}
|
@ -119,3 +119,15 @@ export const settleAllFullfilfedPromises = async <Item, FulfilledItem = Item>(
|
||||
return acc
|
||||
}, [])
|
||||
}
|
||||
|
||||
export const isPromiseFulfilled = <T>(
|
||||
result: PromiseSettledResult<T>
|
||||
): result is PromiseFulfilledResult<T> => {
|
||||
return result.status === 'fulfilled'
|
||||
}
|
||||
|
||||
export const isPromiseRejected = <T>(
|
||||
result: PromiseSettledResult<T>
|
||||
): result is PromiseRejectedResult => {
|
||||
return result.status === 'rejected'
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user
Can we make add the
className={styles.ticket}>
to the icon directly here instead of wrapping the SVG in the code below?