Compare commits

..

26 Commits

Author SHA1 Message Date
aae11589a4 Merge pull request 'issue-166-open-timestamps' (#220) from issue-166-open-timestamps into staging
All checks were successful
Release to Staging / build_and_release (push) Successful in 1m16s
Reviewed-on: #220
Reviewed-by: enes <enes@noreply.git.nostrdev.com>
2024-10-25 11:18:46 +00:00
69f67fc812 chore: disables rules for specific parts of code
All checks were successful
Open PR on Staging / audit_and_check (pull_request) Successful in 30s
2024-10-25 14:15:12 +03:00
38cd88fd86 fix: moves styling to SVG
Some checks failed
Open PR on Staging / audit_and_check (pull_request) Failing after 32s
2024-10-25 13:13:22 +03:00
dbcd54cec0 chore: merge branch 'main' into issue-166-open-timestamps
Some checks failed
Open PR on Staging / audit_and_check (pull_request) Failing after 27s
2024-10-24 15:58:39 +03:00
2d0212fd6c fix: redundant updates
Some checks failed
Open PR on Staging / audit_and_check (pull_request) Failing after 32s
2024-10-24 12:54:47 +03:00
19b815e528 feat(opentimestamps): updates tooltip
Some checks failed
Open PR on Staging / audit_and_check (pull_request) Failing after 27s
2024-10-24 12:42:21 +03:00
b
33e7fc7771 Merge pull request 'Release' (#233) from staging into main
All checks were successful
Release to Production / build_and_release (push) Successful in 1m9s
Reviewed-on: #233
2024-10-18 15:09:39 +00:00
54047740f9 chore: updates packages 2024-10-18 11:26:25 +03:00
7f411f09a7 chore: merge branch 'main' into issue-166-open-timestamps 2024-10-18 11:24:31 +03:00
849e47da00 chore: updates packages 2024-10-18 11:03:51 +03:00
b7bd922af3 fix: removes unneeded notification
Some checks failed
Open PR on Staging / audit_and_check (pull_request) Failing after 31s
2024-10-08 17:04:07 +02:00
f12aaf1c2b feat(opentimestamps): amends to flow to only upgrade users timestamps
Some checks failed
Open PR on Staging / audit_and_check (pull_request) Failing after 32s
2024-10-08 17:01:51 +02:00
3d5006a715 fix: removes retrier and updates notification
Some checks failed
Open PR on Staging / audit_and_check (pull_request) Failing after 30s
2024-10-07 17:24:25 +02:00
f38344b9ac fix: adds notifications 2024-10-07 17:20:00 +02:00
2b630c94b6 feat(opentimestamps): updates the flow and adds notifications 2024-10-07 17:19:32 +02:00
edeb22fb37 chore: updates namings 2024-10-07 17:18:27 +02:00
a2138f1de1 feat(opentimestamps): updates utils and adds comments 2024-10-07 17:18:06 +02:00
85bf907f54 feat(opentimestamps): updates data model 2024-10-07 17:17:37 +02:00
3b447dcf6a chore: merge branch 'main' into issue-166-open-timestamps 2024-10-07 16:18:29 +02:00
21aa25a42a feat(opentimestamps): update the full flow 2024-10-06 15:37:04 +02:00
edbe708b65 feat(opentimestamps): updates data model and useSigitMeta hook 2024-10-02 14:47:32 +02:00
b92790ceed feat(opentimestamps): updates opentimestamps type 2024-09-27 16:03:40 +03:00
7f00f9e8bf feat(opentimestamps): updates signing flow 2024-09-27 16:00:48 +03:00
07f1a15aa1 feat(opentimestamps): refactors to timestamp the nostr event id 2024-09-27 14:18:26 +03:00
85bcfac2e0 feat(opentimestamps): adds timestamps to create flow 2024-09-26 15:54:06 +03:00
edfe9a2954 feat(opentimestamps): adds OTS library and retrier function 2024-09-26 15:50:28 +03:00
16 changed files with 517 additions and 17 deletions

View File

@ -6,7 +6,7 @@ module.exports = {
'plugin:@typescript-eslint/recommended', 'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended' 'plugin:react-hooks/recommended'
], ],
ignorePatterns: ['dist', '.eslintrc.cjs', 'licenseChecker.cjs'], ignorePatterns: ['dist', '.eslintrc.cjs', 'licenseChecker.cjs', "*.min.js"],
parser: '@typescript-eslint/parser', parser: '@typescript-eslint/parser',
plugins: ['react-refresh'], plugins: ['react-refresh'],
rules: { rules: {

View File

@ -8,6 +8,7 @@
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
<script src="/opentimestamps.min.js"></script>
<script type="module" src="/src/main.tsx"></script> <script type="module" src="/src/main.tsx"></script>
</body> </body>
</html> </html>

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "sigit", "name": "sigit",
"version": "0.0.0", "version": "0.0.0-beta",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "sigit", "name": "sigit",
"version": "0.0.0", "version": "0.0.0-beta",
"hasInstallScript": true, "hasInstallScript": true,
"license": "AGPL-3.0-or-later ", "license": "AGPL-3.0-or-later ",
"dependencies": { "dependencies": {

2
public/opentimestamps.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -4,6 +4,7 @@ import {
fromUnixTimestamp, fromUnixTimestamp,
hexToNpub, hexToNpub,
npubToHex, npubToHex,
SigitStatus,
SignStatus SignStatus
} from '../../utils' } from '../../utils'
import { useSigitMeta } from '../../hooks/useSigitMeta' import { useSigitMeta } from '../../hooks/useSigitMeta'
@ -15,6 +16,8 @@ import {
faCalendar, faCalendar,
faCalendarCheck, faCalendarCheck,
faCalendarPlus, faCalendarPlus,
faCheck,
faClock,
faEye, faEye,
faFile, faFile,
faFileCircleExclamation faFileCircleExclamation
@ -22,7 +25,7 @@ import {
import { getExtensionIconLabel } from '../getExtensionIconLabel' import { getExtensionIconLabel } from '../getExtensionIconLabel'
import { useAppSelector } from '../../hooks/store' import { useAppSelector } from '../../hooks/store'
import { DisplaySigner } from '../DisplaySigner' import { DisplaySigner } from '../DisplaySigner'
import { Meta } from '../../types' import { Meta, OpenTimestamp } from '../../types'
import { extractFileExtensions } from '../../utils/file' import { extractFileExtensions } from '../../utils/file'
import { UserAvatar } from '../UserAvatar' import { UserAvatar } from '../UserAvatar'
@ -42,7 +45,9 @@ export const UsersDetails = ({ meta }: UsersDetailsProps) => {
completedAt, completedAt,
parsedSignatureEvents, parsedSignatureEvents,
signedStatus, signedStatus,
isValid isValid,
id,
timestamps
} = useSigitMeta(meta) } = useSigitMeta(meta)
const { usersPubkey } = useAppSelector((state) => state.auth) const { usersPubkey } = useAppSelector((state) => state.auth)
const userCanSign = const userCanSign =
@ -51,6 +56,50 @@ export const UsersDetails = ({ meta }: UsersDetailsProps) => {
const { extensions, isSame } = extractFileExtensions(Object.keys(fileHashes)) 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} />
} 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 ? ( return submittedBy ? (
<div className={styles.container}> <div className={styles.container}>
<div className={styles.section}> <div className={styles.section}>
@ -115,19 +164,35 @@ export const UsersDetails = ({ meta }: UsersDetailsProps) => {
<p>Details</p> <p>Details</p>
<Tooltip <Tooltip
title={'Publication date'} title={getTimestampTooltipTitle(
'Publication date',
!!(timestamps && id && isTimestampVerified(timestamps, id))
)}
placement="top" placement="top"
arrow arrow
disableInteractive disableInteractive
> >
<span className={styles.detailsItem}> <span className={styles.detailsItem}>
<FontAwesomeIcon icon={faCalendarPlus} />{' '} <FontAwesomeIcon icon={faCalendarPlus} />{' '}
{createdAt ? formatTimestamp(createdAt) : <>&mdash;</>} {createdAt ? formatTimestamp(createdAt) : <>&mdash;</>}{' '}
{timestamps &&
timestamps.length > 0 &&
id &&
getOpenTimestampsInfo(timestamps, id)}
</span> </span>
</Tooltip> </Tooltip>
<Tooltip <Tooltip
title={'Completion date'} title={getTimestampTooltipTitle(
'Completion date',
!!(
signedStatus === SigitStatus.Complete &&
completedAt &&
timestamps &&
timestamps.length > 0 &&
timestamps[timestamps.length - 1].verification
)
)}
placement="top" placement="top"
arrow arrow
disableInteractive disableInteractive
@ -135,13 +200,26 @@ export const UsersDetails = ({ meta }: UsersDetailsProps) => {
<span className={styles.detailsItem}> <span className={styles.detailsItem}>
<FontAwesomeIcon icon={faCalendarCheck} />{' '} <FontAwesomeIcon icon={faCalendarCheck} />{' '}
{completedAt ? formatTimestamp(completedAt) : <>&mdash;</>} {completedAt ? formatTimestamp(completedAt) : <>&mdash;</>}
{signedStatus === SigitStatus.Complete &&
completedAt &&
timestamps &&
timestamps.length > 0 && (
<span className={styles.ticket}>
{getCompletedOpenTimestampsInfo(
timestamps[timestamps.length - 1]
)}
</span>
)}
</span> </span>
</Tooltip> </Tooltip>
{/* User signed date */} {/* User signed date */}
{userCanSign ? ( {userCanSign ? (
<Tooltip <Tooltip
title={'Your signature date'} title={getTimestampTooltipTitle(
'Your signature date',
isUserSignatureTimestampVerified()
)}
placement="top" placement="top"
arrow arrow
disableInteractive disableInteractive
@ -161,6 +239,16 @@ export const UsersDetails = ({ meta }: UsersDetailsProps) => {
) : ( ) : (
<>&mdash;</> <>&mdash;</>
)} )}
{hexToNpub(usersPubkey) in parsedSignatureEvents &&
timestamps &&
timestamps.length > 0 && (
<span className={styles.ticket}>
{getOpenTimestampsInfo(
timestamps,
parsedSignatureEvents[hexToNpub(usersPubkey)].id
)}
</span>
)}
</span> </span>
</Tooltip> </Tooltip>
) : null} ) : null}

View File

@ -31,8 +31,6 @@
padding: 5px; padding: 5px;
display: flex; display: flex;
align-items: center;
justify-content: start;
> :first-child { > :first-child {
padding: 5px; padding: 5px;
@ -44,3 +42,7 @@
color: white; color: white;
} }
} }
.ticket {
margin-left: auto;
}

View File

@ -3,7 +3,8 @@ import {
CreateSignatureEventContent, CreateSignatureEventContent,
DocSignatureEvent, DocSignatureEvent,
Meta, Meta,
SignedEventContent SignedEventContent,
OpenTimestamp
} from '../types' } from '../types'
import { Mark } from '../types/mark' import { Mark } from '../types/mark'
import { import {
@ -58,6 +59,8 @@ export interface FlatMeta
signersStatus: { signersStatus: {
[signer: `npub1${string}`]: SignStatus [signer: `npub1${string}`]: SignStatus
} }
timestamps?: OpenTimestamp[]
} }
/** /**
@ -162,7 +165,6 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
setEncryptionKey(decrypted) setEncryptionKey(decrypted)
} }
} }
// Temp. map to hold events and signers // Temp. map to hold events and signers
const parsedSignatureEventsMap = new Map< const parsedSignatureEventsMap = new Map<
`npub1${string}`, `npub1${string}`,
@ -276,6 +278,7 @@ export const useSigitMeta = (meta: Meta): FlatMeta => {
createSignature: meta?.createSignature, createSignature: meta?.createSignature,
docSignatures: meta?.docSignatures, docSignatures: meta?.docSignatures,
keys: meta?.keys, keys: meta?.keys,
timestamps: meta?.timestamps,
isValid, isValid,
kind, kind,
tags, tags,

View File

@ -19,6 +19,7 @@ import {
CreateSignatureEventContent, CreateSignatureEventContent,
Meta, Meta,
ProfileMetadata, ProfileMetadata,
SignedEvent,
User, User,
UserRole UserRole
} from '../../types' } from '../../types'
@ -62,6 +63,7 @@ import {
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
import { getSigitFile, SigitFile } from '../../utils/file.ts' import { getSigitFile, SigitFile } from '../../utils/file.ts'
import _ from 'lodash' import _ from 'lodash'
import { generateTimestamp } from '../../utils/opentimestamps.ts'
export const CreatePage = () => { export const CreatePage = () => {
const navigate = useNavigate() const navigate = useNavigate()
@ -642,6 +644,11 @@ export const CreatePage = () => {
return receivers.map((receiver) => sendNotification(receiver, meta)) 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 () => { const handleCreate = async () => {
try { try {
if (!validateInputs()) return if (!validateInputs()) return
@ -691,6 +698,12 @@ export const CreatePage = () => {
const keys = await generateKeys(pubkeys, encryptionKey) const keys = await generateKeys(pubkeys, encryptionKey)
if (!keys) return if (!keys) return
setLoadingSpinnerDesc('Generating an open timestamp.')
const timestamp = await generateTimestamp(
extractNostrId(createSignature)
)
const meta: Meta = { const meta: Meta = {
createSignature, createSignature,
keys, keys,
@ -698,6 +711,10 @@ export const CreatePage = () => {
docSignatures: {} docSignatures: {}
} }
if (timestamp) {
meta.timestamps = [timestamp]
}
setLoadingSpinnerDesc('Updating user app data') setLoadingSpinnerDesc('Updating user app data')
const event = await updateUsersAppData(meta) const event = await updateUsersAppData(meta)
if (!event) return if (!event) return

View File

@ -53,6 +53,7 @@ import {
SigitFile SigitFile
} from '../../utils/file.ts' } from '../../utils/file.ts'
import { ARRAY_BUFFER, DEFLATE } from '../../utils/const.ts' import { ARRAY_BUFFER, DEFLATE } from '../../utils/const.ts'
import { generateTimestamp } from '../../utils/opentimestamps.ts'
enum SignedStatus { enum SignedStatus {
Fully_Signed, Fully_Signed,
@ -566,6 +567,14 @@ export const SignPage = () => {
const updatedMeta = updateMetaSignatures(meta, signedEvent) 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()) { if (await isOnline()) {
await handleOnlineFlow(updatedMeta) await handleOnlineFlow(updatedMeta)
} else { } else {

View File

@ -5,7 +5,13 @@ import { useEffect, useRef, useState } from 'react'
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
import { LoadingSpinner } from '../../components/LoadingSpinner' import { LoadingSpinner } from '../../components/LoadingSpinner'
import { NostrController } from '../../controllers' import { NostrController } from '../../controllers'
import { DocSignatureEvent, Meta } from '../../types' import {
DocSignatureEvent,
Meta,
SignedEvent,
OpenTimestamp,
OpenTimestampUpgradeVerifyResponse
} from '../../types'
import { import {
decryptArrayBuffer, decryptArrayBuffer,
getHash, getHash,
@ -14,7 +20,10 @@ import {
parseJson, parseJson,
readContentOfZipEntry, readContentOfZipEntry,
signEventForMetaFile, signEventForMetaFile,
getCurrentUserFiles getCurrentUserFiles,
updateUsersAppData,
npubToHex,
sendNotification
} from '../../utils' } from '../../utils'
import styles from './style.module.scss' import styles from './style.module.scss'
import { useLocation } from 'react-router-dom' import { useLocation } from 'react-router-dom'
@ -26,7 +35,7 @@ import { saveAs } from 'file-saver'
import { Container } from '../../components/Container' import { Container } from '../../components/Container'
import { useSigitMeta } from '../../hooks/useSigitMeta.tsx' import { useSigitMeta } from '../../hooks/useSigitMeta.tsx'
import { StickySideColumns } from '../../layouts/StickySideColumns.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 FileList from '../../components/FileList'
import { CurrentUserFile } from '../../types/file.ts' import { CurrentUserFile } from '../../types/file.ts'
import { Mark } from '../../types/mark.ts' import { Mark } from '../../types/mark.ts'
@ -44,6 +53,8 @@ import {
faFile, faFile,
faFileDownload faFileDownload
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
import { upgradeAndVerifyTimestamp } from '../../utils/opentimestamps.ts'
import _ from 'lodash'
interface PdfViewProps { interface PdfViewProps {
files: CurrentUserFile[] files: CurrentUserFile[]
@ -176,7 +187,8 @@ export const VerifyPage = () => {
signers, signers,
viewers, viewers,
fileHashes, fileHashes,
parsedSignatureEvents parsedSignatureEvents,
timestamps
} = useSigitMeta(meta) } = useSigitMeta(meta)
const [files, setFiles] = useState<{ [filename: string]: SigitFile }>({}) const [files, setFiles] = useState<{ [filename: string]: SigitFile }>({})
@ -186,6 +198,16 @@ export const VerifyPage = () => {
[key: string]: string | null [key: string]: string | null
}>({}) }>({})
const signTimestampEvent = async (signerContent: {
timestamps: OpenTimestamp[]
}): Promise<SignedEvent | null> => {
return await signEventForMetaFile(
JSON.stringify(signerContent),
nostrController,
setIsLoading
)
}
useEffect(() => { useEffect(() => {
if (Object.entries(files).length > 0) { if (Object.entries(files).length > 0) {
const tmp = getCurrentUserFiles(files, currentFileHashes, fileHashes) const tmp = getCurrentUserFiles(files, currentFileHashes, fileHashes)
@ -193,6 +215,147 @@ export const VerifyPage = () => {
} }
}, [currentFileHashes, fileHashes, files]) }, [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(() => { useEffect(() => {
if (metaInNavState && encryptionKey) { if (metaInNavState && encryptionKey) {
const processSigit = async () => { const processSigit = async () => {

View File

@ -18,6 +18,7 @@ export interface Meta {
docSignatures: { [key: `npub1${string}`]: string } docSignatures: { [key: `npub1${string}`]: string }
exportSignature?: string exportSignature?: string
keys?: { sender: string; keys: { [user: `npub1${string}`]: string } } keys?: { sender: string; keys: { [user: `npub1${string}`]: string } }
timestamps?: OpenTimestamp[]
} }
export interface CreateSignatureEventContent { export interface CreateSignatureEventContent {
@ -39,6 +40,25 @@ export interface Sigit {
meta: Meta 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 { export interface UserAppData {
/** /**
* Key will be id of create signature * Key will be id of create signature

38
src/types/opentimestamps.d.ts vendored Normal file
View 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 }
}

View File

@ -3,5 +3,6 @@ import type { WindowNostr } from 'nostr-tools/nip07'
declare global { declare global {
interface Window { interface Window {
nostr?: WindowNostr nostr?: WindowNostr
OpenTimestamps: OpenTimestamps
} }
} }

119
src/utils/opentimestamps.ts Normal file
View 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
View 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)
)
}
}

View File

@ -119,3 +119,15 @@ export const settleAllFullfilfedPromises = async <Item, FulfilledItem = Item>(
return acc 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'
}