Compare commits

..

27 Commits

Author SHA1 Message Date
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
b
7056ad3cd3 Merge pull request 'Release to main' (#216) from staging into main
All checks were successful
Release to Production / build_and_release (push) Successful in 1m16s
Reviewed-on: #216
2024-10-01 20:19:09 +00:00
b
7dffe75bd7 Merge branch 'main' into staging
All checks were successful
Release to Staging / build_and_release (push) Successful in 1m24s
2024-10-01 20:15:23 +00:00
8da2510a18 Merge pull request #206 from 201-toolbox-update into staging
All checks were successful
Release to Staging / build_and_release (push) Successful in 1m20s
Reviewed-on: #206
Reviewed-by: eugene <eugene@nostrdev.com>
2024-09-30 14:20:07 +00:00
633c23e459 refactor: center banner notice text
All checks were successful
Open PR on Staging / audit_and_check (pull_request) Successful in 34s
2024-09-20 11:29:35 +02:00
2e1d48168a refactor: rename userId to npub
All checks were successful
Open PR on Staging / audit_and_check (pull_request) Successful in 34s
2024-09-20 11:13:48 +02:00
e05d3e53a2 fix: remove duplicate states and fix default signer 2024-09-20 10:26:32 +02:00
d8d51be603 fix: add small avatar when select is not showing 2024-09-19 15:05:03 +02:00
5f92906032 fix: add file and page index, hide select if not active 2024-09-19 15:02:19 +02:00
70cca9dd10 refactor: add getProfileUsername utility func 2024-09-19 14:46:22 +02:00
9bae5b9ba2 Merge branch 'staging' into 201-toolbox-update
All checks were successful
Open PR on Staging / audit_and_check (pull_request) Successful in 33s
2024-09-19 11:31:03 +00:00
a1bf88d243 chore(git): merge pull request #207 from 145-default-signer into 201-toolbox-update
All checks were successful
Open PR on Staging / audit_and_check (pull_request) Successful in 34s
Reviewed-on: #207
2024-09-19 11:24:16 +00:00
67c3c74515 Merge branch '201-toolbox-update' into 145-default-signer 2024-09-19 11:20:05 +00:00
f81f2b0523 refactor: better variable names
All checks were successful
Open PR on Staging / audit_and_check (pull_request) Successful in 33s
2024-09-19 13:15:54 +02:00
182ef40d8d Merge branch 'staging' into 201-toolbox-update
All checks were successful
Open PR on Staging / audit_and_check (pull_request) Successful in 34s
2024-09-19 10:00:45 +00:00
39934f59c3 fix: last signer as default next
All checks were successful
Open PR on Staging / audit_and_check (pull_request) Successful in 34s
2024-09-19 11:46:34 +02:00
b
dd97dfbaf0 Merge pull request 'New release' (#210) from staging into main
All checks were successful
Release to Production / build_and_release (push) Successful in 1m11s
Reviewed-on: #210
Reviewed-by: eugene <eugene@nostrdev.com>
2024-09-19 08:23:14 +00:00
dfdcb8419d fix(marks): add default signer
All checks were successful
Open PR on Staging / audit_and_check (pull_request) Successful in 33s
2024-09-17 17:27:37 +02:00
f8a4480994 refactor(toolbox): reduce number of mark types
All checks were successful
Open PR on Staging / audit_and_check (pull_request) Successful in 33s
Closes #201
2024-09-17 17:22:15 +02:00
17 changed files with 576 additions and 244 deletions

View File

@ -28,9 +28,9 @@ import {
} from '../../routes' } from '../../routes'
import { import {
clearAuthToken, clearAuthToken,
getProfileUsername,
hexToNpub, hexToNpub,
saveNsecBunkerDelegatedKey, saveNsecBunkerDelegatedKey
shorten
} from '../../utils' } from '../../utils'
import styles from './style.module.scss' import styles from './style.module.scss'
import { setUserRobotImage } from '../../store/userRobotImage/action' import { setUserRobotImage } from '../../store/userRobotImage/action'
@ -58,9 +58,8 @@ export const AppBar = () => {
useEffect(() => { useEffect(() => {
if (metadataState) { if (metadataState) {
if (metadataState.content) { if (metadataState.content) {
const { picture, display_name, name } = JSON.parse( const profileMetadata = JSON.parse(metadataState.content)
metadataState.content const { picture } = profileMetadata
)
if (picture || userRobotImage) { if (picture || userRobotImage) {
setUserAvatar(picture || userRobotImage) setUserAvatar(picture || userRobotImage)
@ -70,7 +69,7 @@ export const AppBar = () => {
? hexToNpub(authState.usersPubkey) ? hexToNpub(authState.usersPubkey)
: '' : ''
setUsername(shorten(display_name || name || npub, 7)) setUsername(getProfileUsername(npub, profileMetadata))
} else { } else {
setUserAvatar(userRobotImage || '') setUserAvatar(userRobotImage || '')
setUsername('') setUsername('')
@ -133,8 +132,10 @@ export const AppBar = () => {
<div className={styles.banner}> <div className={styles.banner}>
<Container> <Container>
<div className={styles.bannerInner}> <div className={styles.bannerInner}>
<p className={styles.bannerText}>
SIGit is currently Alpha software (available for internal SIGit is currently Alpha software (available for internal
testing), use at your own risk! testing), use at your own risk!
</p>
<Button <Button
aria-label={`close banner`} aria-label={`close banner`}
variant="text" variant="text"

View File

@ -67,3 +67,9 @@
} }
} }
} }
.bannerText {
margin-left: 54px;
flex-grow: 1;
text-align: center;
}

View File

@ -11,16 +11,16 @@ import styles from './style.module.scss'
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { ProfileMetadata, User, UserRole } from '../../types' import { ProfileMetadata, User, UserRole } from '../../types'
import { MouseState, PdfPage, DrawnField, DrawTool } from '../../types/drawing' import { MouseState, PdfPage, DrawnField, DrawTool } from '../../types/drawing'
import { truncate } from 'lodash' import { hexToNpub, npubToHex, getProfileUsername } from '../../utils'
import { settleAllFullfilfedPromises, hexToNpub, npubToHex } from '../../utils' import { SigitFile } from '../../utils/file'
import { getSigitFile, SigitFile } from '../../utils/file'
import { getToolboxLabelByMarkType } from '../../utils/mark' import { getToolboxLabelByMarkType } from '../../utils/mark'
import { FileDivider } from '../FileDivider' import { FileDivider } from '../FileDivider'
import { ExtensionFileBox } from '../ExtensionFileBox' import { ExtensionFileBox } from '../ExtensionFileBox'
import { FONT_SIZE, FONT_TYPE, inPx } from '../../utils/pdf' import { FONT_SIZE, FONT_TYPE, inPx } from '../../utils/pdf'
import { useScale } from '../../hooks/useScale' import { useScale } from '../../hooks/useScale'
import { AvatarIconButton } from '../UserAvatarIconButton' import { AvatarIconButton } from '../UserAvatarIconButton'
import { LoadingSpinner } from '../LoadingSpinner' import { UserAvatar } from '../UserAvatar'
import _ from 'lodash'
const DEFAULT_START_SIZE = { const DEFAULT_START_SIZE = {
width: 140, width: 140,
@ -28,52 +28,50 @@ const DEFAULT_START_SIZE = {
} as const } as const
interface Props { interface Props {
selectedFiles: File[]
users: User[] users: User[]
metadata: { [key: string]: ProfileMetadata } metadata: { [key: string]: ProfileMetadata }
onDrawFieldsChange: (sigitFiles: SigitFile[]) => void sigitFiles: SigitFile[]
setSigitFiles: React.Dispatch<React.SetStateAction<SigitFile[]>>
selectedTool?: DrawTool selectedTool?: DrawTool
} }
export const DrawPDFFields = (props: Props) => { export const DrawPDFFields = (props: Props) => {
const { selectedFiles, selectedTool, onDrawFieldsChange, users } = props const { selectedTool, sigitFiles, setSigitFiles, users } = props
const { to, from } = useScale()
const [sigitFiles, setSigitFiles] = useState<SigitFile[]>([]) const signers = users.filter((u) => u.role === UserRole.signer)
const [parsingPdf, setIsParsing] = useState<boolean>(false) const defaultSignerNpub = signers.length ? hexToNpub(signers[0].pubkey) : ''
const [lastSigner, setLastSigner] = useState(defaultSignerNpub)
/**
* Return first pubkey that is present in the signers list
* @param pubkeys
* @returns available pubkey or empty string
*/
const getAvailableSigner = (...pubkeys: string[]) => {
const availableSigner: string | undefined = pubkeys.find((pubkey) =>
signers.some((s) => s.pubkey === npubToHex(pubkey))
)
return availableSigner || ''
}
const { to, from } = useScale()
const [mouseState, setMouseState] = useState<MouseState>({ const [mouseState, setMouseState] = useState<MouseState>({
clicked: false clicked: false
}) })
const [activeDrawField, setActiveDrawField] = useState<number>() const [activeDrawnField, setActiveDrawnField] = useState<{
fileIndex: number
useEffect(() => { pageIndex: number
if (selectedFiles) { drawnFieldIndex: number
/** }>()
* Reads the binary files and converts to internal file type const isActiveDrawnField = (
* and sets to a state (adds images if it's a PDF) fileIndex: number,
*/ pageIndex: number,
const parsePages = async () => { drawnFieldIndex: number
const files = await settleAllFullfilfedPromises( ) =>
selectedFiles, activeDrawnField?.fileIndex === fileIndex &&
getSigitFile activeDrawnField?.pageIndex === pageIndex &&
) activeDrawnField?.drawnFieldIndex === drawnFieldIndex
setSigitFiles(files)
}
setIsParsing(true)
parsePages().finally(() => {
setIsParsing(false)
})
}
}, [selectedFiles])
useEffect(() => {
if (sigitFiles) onDrawFieldsChange(sigitFiles)
}, [onDrawFieldsChange, sigitFiles])
/** /**
* Drawing events * Drawing events
@ -100,7 +98,12 @@ export const DrawPDFFields = (props: Props) => {
* @param event Pointer event * @param event Pointer event
* @param page PdfPage where press happened * @param page PdfPage where press happened
*/ */
const handlePointerDown = (event: React.PointerEvent, page: PdfPage) => { const handlePointerDown = (
event: React.PointerEvent,
page: PdfPage,
fileIndex: number,
pageIndex: number
) => {
// Proceed only if left click // Proceed only if left click
if (event.button !== 0) return if (event.button !== 0) return
@ -115,12 +118,17 @@ export const DrawPDFFields = (props: Props) => {
top: to(page.width, y), top: to(page.width, y),
width: event.pointerType === 'mouse' ? 0 : DEFAULT_START_SIZE.width, width: event.pointerType === 'mouse' ? 0 : DEFAULT_START_SIZE.width,
height: event.pointerType === 'mouse' ? 0 : DEFAULT_START_SIZE.height, height: event.pointerType === 'mouse' ? 0 : DEFAULT_START_SIZE.height,
counterpart: '', counterpart: getAvailableSigner(lastSigner, defaultSignerNpub),
type: selectedTool.identifier type: selectedTool.identifier
} }
page.drawnFields.push(newField) page.drawnFields.push(newField)
setActiveDrawnField({
fileIndex,
pageIndex,
drawnFieldIndex: page.drawnFields.length - 1
})
setMouseState((prev) => { setMouseState((prev) => {
return { return {
...prev, ...prev,
@ -189,6 +197,8 @@ export const DrawPDFFields = (props: Props) => {
*/ */
const handleDrawnFieldPointerDown = ( const handleDrawnFieldPointerDown = (
event: React.PointerEvent, event: React.PointerEvent,
fileIndex: number,
pageIndex: number,
drawnFieldIndex: number drawnFieldIndex: number
) => { ) => {
event.stopPropagation() event.stopPropagation()
@ -198,7 +208,7 @@ export const DrawPDFFields = (props: Props) => {
const drawingRectangleCoords = getPointerCoordinates(event) const drawingRectangleCoords = getPointerCoordinates(event)
setActiveDrawField(drawnFieldIndex) setActiveDrawnField({ fileIndex, pageIndex, drawnFieldIndex })
setMouseState({ setMouseState({
dragging: true, dragging: true,
clicked: false, clicked: false,
@ -254,13 +264,15 @@ export const DrawPDFFields = (props: Props) => {
*/ */
const handleResizePointerDown = ( const handleResizePointerDown = (
event: React.PointerEvent, event: React.PointerEvent,
fileIndex: number,
pageIndex: number,
drawnFieldIndex: number drawnFieldIndex: number
) => { ) => {
// Proceed only if left click // Proceed only if left click
if (event.button !== 0) return if (event.button !== 0) return
event.stopPropagation() event.stopPropagation()
setActiveDrawField(drawnFieldIndex) setActiveDrawnField({ fileIndex, pageIndex, drawnFieldIndex })
setMouseState({ setMouseState({
resizing: true resizing: true
}) })
@ -369,7 +381,7 @@ export const DrawPDFFields = (props: Props) => {
handlePointerMove(event, page) handlePointerMove(event, page)
}} }}
onPointerDown={(event) => { onPointerDown={(event) => {
handlePointerDown(event, page) handlePointerDown(event, page, fileIndex, pageIndex)
}} }}
draggable="false" draggable="false"
src={page.image} src={page.image}
@ -381,7 +393,12 @@ export const DrawPDFFields = (props: Props) => {
<div <div
key={drawnFieldIndex} key={drawnFieldIndex}
onPointerDown={(event) => onPointerDown={(event) =>
handleDrawnFieldPointerDown(event, drawnFieldIndex) handleDrawnFieldPointerDown(
event,
fileIndex,
pageIndex,
drawnFieldIndex
)
} }
onPointerMove={(event) => { onPointerMove={(event) => {
handleDrawnFieldPointerMove(event, drawnField, page.width) handleDrawnFieldPointerMove(event, drawnField, page.width)
@ -402,7 +419,11 @@ export const DrawPDFFields = (props: Props) => {
touchAction: 'none', touchAction: 'none',
opacity: opacity:
mouseState.dragging && mouseState.dragging &&
activeDrawField === drawnFieldIndex isActiveDrawnField(
fileIndex,
pageIndex,
drawnFieldIndex
)
? 0.8 ? 0.8
: undefined : undefined
}} }}
@ -419,7 +440,12 @@ export const DrawPDFFields = (props: Props) => {
</div> </div>
<span <span
onPointerDown={(event) => onPointerDown={(event) =>
handleResizePointerDown(event, drawnFieldIndex) handleResizePointerDown(
event,
fileIndex,
pageIndex,
drawnFieldIndex
)
} }
onPointerMove={(event) => { onPointerMove={(event) => {
handleResizePointerMove(event, drawnField, page.width) handleResizePointerMove(event, drawnField, page.width)
@ -428,7 +454,11 @@ export const DrawPDFFields = (props: Props) => {
style={{ style={{
background: background:
mouseState.resizing && mouseState.resizing &&
activeDrawField === drawnFieldIndex isActiveDrawnField(
fileIndex,
pageIndex,
drawnFieldIndex
)
? 'var(--primary-main)' ? 'var(--primary-main)'
: undefined : undefined
}} }}
@ -446,6 +476,23 @@ export const DrawPDFFields = (props: Props) => {
> >
<Close fontSize="small" /> <Close fontSize="small" />
</span> </span>
{!isActiveDrawnField(
fileIndex,
pageIndex,
drawnFieldIndex
) &&
!!drawnField.counterpart && (
<div className={styles.counterpartAvatar}>
<UserAvatar
pubkey={npubToHex(drawnField.counterpart)!}
/>
</div>
)}
{isActiveDrawnField(
fileIndex,
pageIndex,
drawnFieldIndex
) && (
<div <div
onPointerDown={handleUserSelectPointerDown} onPointerDown={handleUserSelectPointerDown}
className={styles.userSelect} className={styles.userSelect}
@ -453,9 +500,10 @@ export const DrawPDFFields = (props: Props) => {
<FormControl fullWidth size="small"> <FormControl fullWidth size="small">
<InputLabel id="counterparts">Counterpart</InputLabel> <InputLabel id="counterparts">Counterpart</InputLabel>
<Select <Select
value={drawnField.counterpart} value={getAvailableSigner(drawnField.counterpart)}
onChange={(event) => { onChange={(event) => {
drawnField.counterpart = event.target.value drawnField.counterpart = event.target.value
setLastSigner(event.target.value)
refreshPdfFiles() refreshPdfFiles()
}} }}
labelId="counterparts" labelId="counterparts"
@ -463,39 +511,24 @@ export const DrawPDFFields = (props: Props) => {
sx={{ sx={{
background: 'white' background: 'white'
}} }}
renderValue={(value) => renderCounterpartValue(value)} renderValue={(value) =>
renderCounterpartValue(value)
}
> >
{users {signers.map((signer, index) => {
.filter((u) => u.role === UserRole.signer) const npub = hexToNpub(signer.pubkey)
.map((user, index) => { const metadata = props.metadata[signer.pubkey]
const npub = hexToNpub(user.pubkey) const displayValue = getProfileUsername(
let displayValue = truncate(npub, {
length: 16
})
const metadata = props.metadata[user.pubkey]
if (metadata) {
displayValue = truncate(
metadata.name ||
metadata.display_name ||
metadata.username ||
npub, npub,
{ metadata
length: 16
}
) )
}
return ( return (
<MenuItem <MenuItem key={index} value={npub}>
key={index}
value={hexToNpub(user.pubkey)}
>
<ListItemIcon> <ListItemIcon>
<AvatarIconButton <AvatarIconButton
src={metadata?.picture} src={metadata?.picture}
hexKey={user.pubkey} hexKey={signer.pubkey}
aria-label={`account of user ${displayValue}`} aria-label={`account of user ${displayValue}`}
color="inherit" color="inherit"
sx={{ sx={{
@ -514,6 +547,7 @@ export const DrawPDFFields = (props: Props) => {
</Select> </Select>
</FormControl> </FormControl>
</div> </div>
)}
</div> </div>
) )
})} })}
@ -524,28 +558,19 @@ export const DrawPDFFields = (props: Props) => {
) )
} }
const renderCounterpartValue = (value: string) => { const renderCounterpartValue = (npub: string) => {
const user = users.find((u) => u.pubkey === npubToHex(value)) let displayValue = _.truncate(npub, { length: 16 })
if (user) {
let displayValue = truncate(value, {
length: 16
})
const metadata = props.metadata[user.pubkey] const signer = signers.find((u) => u.pubkey === npubToHex(npub))
if (signer) {
const metadata = props.metadata[signer.pubkey]
displayValue = getProfileUsername(npub, metadata)
if (metadata) {
displayValue = truncate(
metadata.name || metadata.display_name || metadata.username || value,
{
length: 16
}
)
}
return ( return (
<> <div className={styles.counterpartSelectValue}>
<AvatarIconButton <AvatarIconButton
src={props.metadata[user.pubkey]?.picture} src={props.metadata[signer.pubkey]?.picture}
hexKey={npubToHex(user.pubkey) || undefined} hexKey={signer.pubkey || undefined}
sx={{ sx={{
padding: 0, padding: 0,
marginRight: '6px', marginRight: '6px',
@ -556,19 +581,11 @@ export const DrawPDFFields = (props: Props) => {
}} }}
/> />
{displayValue} {displayValue}
</> </div>
) )
} }
return value return displayValue
}
if (parsingPdf) {
return <LoadingSpinner variant="small" />
}
if (!sigitFiles.length) {
return ''
} }
return ( return (
@ -589,7 +606,7 @@ export const DrawPDFFields = (props: Props) => {
<ExtensionFileBox extension={file.extension} /> <ExtensionFileBox extension={file.extension} />
)} )}
</div> </div>
{i < selectedFiles.length - 1 && <FileDivider />} {i < sigitFiles.length - 1 && <FileDivider />}
</React.Fragment> </React.Fragment>
) )
})} })}

View File

@ -84,3 +84,14 @@
padding: 5px 0; padding: 5px 0;
} }
} }
.counterpartSelectValue {
display: flex;
}
.counterpartAvatar {
img {
width: 21px;
height: 21px;
}
}

View File

@ -5,7 +5,7 @@ import { AvatarIconButton } from '../UserAvatarIconButton'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import { useProfileMetadata } from '../../hooks/useProfileMetadata' import { useProfileMetadata } from '../../hooks/useProfileMetadata'
import { Tooltip } from '@mui/material' import { Tooltip } from '@mui/material'
import { shorten } from '../../utils' import { getProfileUsername } from '../../utils'
import { TooltipChild } from '../TooltipChild' import { TooltipChild } from '../TooltipChild'
interface UserAvatarProps { interface UserAvatarProps {
@ -22,7 +22,7 @@ export const UserAvatar = ({
isNameVisible = false isNameVisible = false
}: UserAvatarProps) => { }: UserAvatarProps) => {
const profile = useProfileMetadata(pubkey) const profile = useProfileMetadata(pubkey)
const name = profile?.display_name || profile?.name || shorten(pubkey) const name = getProfileUsername(pubkey, profile)
const image = profile?.picture const image = profile?.picture
return ( return (

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 {
@ -59,6 +60,8 @@ export interface FlatMeta
signersStatus: { signersStatus: {
[signer: `npub1${string}`]: SignStatus [signer: `npub1${string}`]: SignStatus
} }
timestamps?: OpenTimestamp[]
} }
/** /**
@ -163,7 +166,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}`,
@ -277,6 +279,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

@ -40,7 +40,8 @@ import {
signEventForMetaFile, signEventForMetaFile,
updateUsersAppData, updateUsersAppData,
uploadToFileStorage, uploadToFileStorage,
DEFAULT_TOOLBOX DEFAULT_TOOLBOX,
settleAllFullfilfedPromises
} from '../../utils' } from '../../utils'
import { Container } from '../../components/Container' import { Container } from '../../components/Container'
import fileListStyles from '../../components/FileList/style.module.scss' import fileListStyles from '../../components/FileList/style.module.scss'
@ -61,7 +62,8 @@ import {
faTrash, faTrash,
faUpload faUpload
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
import { SigitFile } from '../../utils/file.ts' import { getSigitFile, SigitFile } from '../../utils/file.ts'
import _ from 'lodash'
import { generateTimestamp } from '../../utils/opentimestamps.ts' import { generateTimestamp } from '../../utils/opentimestamps.ts'
export const CreatePage = () => { export const CreatePage = () => {
@ -108,9 +110,31 @@ export const CreatePage = () => {
{} {}
) )
const [drawnFiles, setDrawnFiles] = useState<SigitFile[]>([]) const [drawnFiles, setDrawnFiles] = useState<SigitFile[]>([])
const [parsingPdf, setIsParsing] = useState<boolean>(false)
useEffect(() => {
if (selectedFiles) {
/**
* Reads the binary files and converts to internal file type
* and sets to a state (adds images if it's a PDF)
*/
const parsePages = async () => {
const files = await settleAllFullfilfedPromises(
selectedFiles,
getSigitFile
)
setDrawnFiles(files)
}
setIsParsing(true)
parsePages().finally(() => {
setIsParsing(false)
})
}
}, [selectedFiles])
const [selectedTool, setSelectedTool] = useState<DrawTool>() const [selectedTool, setSelectedTool] = useState<DrawTool>()
const [toolbox] = useState<DrawTool[]>(DEFAULT_TOOLBOX)
/** /**
* Changes the drawing tool * Changes the drawing tool
@ -284,6 +308,19 @@ export const CreatePage = () => {
const handleRemoveUser = (pubkey: string) => { const handleRemoveUser = (pubkey: string) => {
setUsers((prev) => prev.filter((user) => user.pubkey !== pubkey)) setUsers((prev) => prev.filter((user) => user.pubkey !== pubkey))
// Set counterpart to ''
const drawnFilesCopy = _.cloneDeep(drawnFiles)
drawnFilesCopy.forEach((s) => {
s.pages?.forEach((p) => {
p.drawnFields.forEach((d) => {
if (d.counterpart === hexToNpub(pubkey)) {
d.counterpart = ''
}
})
})
})
setDrawnFiles(drawnFilesCopy)
} }
/** /**
@ -654,6 +691,8 @@ 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( const timestamp = await generateTimestamp(
extractNostrId(createSignature) extractNostrId(createSignature)
) )
@ -739,10 +778,6 @@ export const CreatePage = () => {
} }
} }
const onDrawFieldsChange = (sigitFiles: SigitFile[]) => {
setDrawnFiles(sigitFiles)
}
if (authUrl) { if (authUrl) {
return ( return (
<iframe <iframe
@ -856,20 +891,27 @@ export const CreatePage = () => {
</div> </div>
<div className={`${styles.paperGroup} ${styles.toolbox}`}> <div className={`${styles.paperGroup} ${styles.toolbox}`}>
{toolbox.map((drawTool: DrawTool, index: number) => { {DEFAULT_TOOLBOX.filter((drawTool) => !drawTool.isHidden).map(
(drawTool: DrawTool, index: number) => {
return ( return (
<div <div
key={index} key={index}
{...(drawTool.active && { {...(!drawTool.isComingSoon && {
onClick: () => handleToolSelect(drawTool) onClick: () => handleToolSelect(drawTool)
})} })}
className={`${styles.toolItem} ${selectedTool?.identifier === drawTool.identifier ? styles.selected : ''} ${!drawTool.active ? styles.comingSoon : ''} className={`${styles.toolItem} ${selectedTool?.identifier === drawTool.identifier ? styles.selected : ''} ${drawTool.isComingSoon ? styles.comingSoon : ''}
`} `}
> >
<FontAwesomeIcon fontSize={'15px'} icon={drawTool.icon} /> <FontAwesomeIcon
fontSize={'15px'}
icon={drawTool.icon}
/>
{drawTool.label} {drawTool.label}
{drawTool.active ? ( {!drawTool.isComingSoon ? (
<FontAwesomeIcon fontSize={'15px'} icon={faEllipsis} /> <FontAwesomeIcon
fontSize={'15px'}
icon={faEllipsis}
/>
) : ( ) : (
<span className={styles.comingSoonPlaceholder}> <span className={styles.comingSoonPlaceholder}>
Coming soon Coming soon
@ -877,7 +919,8 @@ export const CreatePage = () => {
)} )}
</div> </div>
) )
})} }
)}
</div> </div>
<Button onClick={handleCreate} variant="contained"> <Button onClick={handleCreate} variant="contained">
@ -893,13 +936,17 @@ export const CreatePage = () => {
centerIcon={faFile} centerIcon={faFile}
rightIcon={faToolbox} rightIcon={faToolbox}
> >
{parsingPdf ? (
<LoadingSpinner variant="small" />
) : (
<DrawPDFFields <DrawPDFFields
metadata={metadata}
users={users} users={users}
selectedFiles={selectedFiles} metadata={metadata}
onDrawFieldsChange={onDrawFieldsChange}
selectedTool={selectedTool} selectedTool={selectedTool}
sigitFiles={drawnFiles}
setSigitFiles={setDrawnFiles}
/> />
)}
</StickySideColumns> </StickySideColumns>
</Container> </Container>
</> </>

View File

@ -1,7 +1,6 @@
import ContentCopyIcon from '@mui/icons-material/ContentCopy' import ContentCopyIcon from '@mui/icons-material/ContentCopy'
import EditIcon from '@mui/icons-material/Edit' import EditIcon from '@mui/icons-material/Edit'
import { Box, IconButton, SxProps, Theme, Typography } from '@mui/material' import { Box, IconButton, SxProps, Theme, Typography } from '@mui/material'
import { truncate } from 'lodash'
import { Event, VerifiedEvent, kinds, nip19 } from 'nostr-tools' import { Event, VerifiedEvent, kinds, nip19 } from 'nostr-tools'
import { useEffect, useMemo, useState } from 'react' import { useEffect, useMemo, useState } from 'react'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
@ -14,6 +13,7 @@ import { State } from '../../store/rootReducer'
import { NostrJoiningBlock, ProfileMetadata } from '../../types' import { NostrJoiningBlock, ProfileMetadata } from '../../types'
import { import {
getNostrJoiningBlockNumber, getNostrJoiningBlockNumber,
getProfileUsername,
getRoboHashPicture, getRoboHashPicture,
hexToNpub, hexToNpub,
shorten shorten
@ -42,15 +42,7 @@ export const ProfilePage = () => {
const [isLoading, setIsLoading] = useState(true) const [isLoading, setIsLoading] = useState(true)
const [loadingSpinnerDesc] = useState('Fetching metadata') const [loadingSpinnerDesc] = useState('Fetching metadata')
const profileName = const profileName = pubkey && getProfileUsername(pubkey, profileMetadata)
pubkey &&
profileMetadata &&
truncate(
profileMetadata.display_name || profileMetadata.name || hexToNpub(pubkey),
{
length: 16
}
)
useEffect(() => { useEffect(() => {
if (npub) { if (npub) {

View File

@ -550,6 +550,8 @@ export const SignPage = () => {
const updatedMeta = updateMetaSignatures(meta, signedEvent) const updatedMeta = updateMetaSignatures(meta, signedEvent)
setLoadingSpinnerDesc('Generating an open timestamp.')
const timestamp = await generateTimestamp(signedEvent.id) const timestamp = await generateTimestamp(signedEvent.id)
if (timestamp) { if (timestamp) {
updatedMeta.timestamps = [...(updatedMeta.timestamps || []), timestamp] updatedMeta.timestamps = [...(updatedMeta.timestamps || []), timestamp]

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,
extractMarksFromSignedMeta, extractMarksFromSignedMeta,
@ -15,7 +21,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'
@ -34,7 +43,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'
@ -48,6 +57,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[]
@ -180,7 +191,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 }>({})
@ -190,6 +202,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)
@ -198,6 +220,115 @@ export const VerifyPage = () => {
}, [currentFileHashes, fileHashes, files]) }, [currentFileHashes, fileHashes, files])
useEffect(() => { useEffect(() => {
console.log('timestamps: ', timestamps)
if (timestamps && timestamps.length > 0) {
console.log(timestamps.every((t) => !!t.verification))
if (timestamps.every((t) => !!t.verification)) {
toast.success('All of your timestamps are fully verified on Bitcoin.')
return
}
const upgradeT = async (timestamps: OpenTimestamp[]) => {
try {
setLoadingSpinnerDesc('Upgrading and verifying your timestamps.')
const verifiedResults = await Promise.all(
timestamps.map(upgradeAndVerifyTimestamp)
)
/**
* 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 upgradedTimestamps = verifiedResults
.filter((t) => t.upgraded || isNewlyVerified(t, timestamps))
.map((t) => {
const timestamp = t.timestamp
if (t.verified) {
timestamp.verification = t.verification
}
return timestamp
})
if (upgradedTimestamps.length === 0) {
toast.success('No timestamp upgrades found.')
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)
}
}, [timestamps])
useEffect(() => {
console.log('this runs')
if (metaInNavState && encryptionKey) { if (metaInNavState && encryptionKey) {
const processSigit = async () => { const processSigit = async () => {
setIsLoading(true) setIsLoading(true)

View File

@ -18,7 +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?: Timestamp[] timestamps?: OpenTimestamp[]
} }
export interface CreateSignatureEventContent { export interface CreateSignatureEventContent {
@ -40,9 +40,23 @@ export interface Sigit {
meta: Meta meta: Meta
} }
export interface Timestamp { export interface OpenTimestamp {
nostrId: string nostrId: string
timestamp: 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 {

View File

@ -31,7 +31,10 @@ export interface DrawTool {
icon: IconDefinition icon: IconDefinition
defaultValue?: string defaultValue?: string
selected?: boolean selected?: boolean
active?: boolean /** show or hide the toolbox item */
isHidden?: boolean
/** show or hide "coming soon" message on the toolbox item */
isComingSoon?: boolean
} }
export enum MarkType { export enum MarkType {

View File

@ -3,13 +3,17 @@ interface OpenTimestamps {
DetachedTimestampFile: { DetachedTimestampFile: {
fromHash(op: any, hash: Uint8Array): any fromHash(op: any, hash: Uint8Array): any
fromBytes(op: any, buffer: Uint8Array): any fromBytes(op: any, buffer: Uint8Array): any
deserialize(buffer: any): any
} }
// Stamp the provided timestamp file and return a Promise // Stamp the provided timestamp file and return a Promise
stamp(file: any): Promise<void> stamp(file: any): Promise<void>
// Verify the provided timestamp proof file // Verify the provided timestamp proof file
verify(file: any): Promise<void> verify(
ots: string,
file: string
): Promise<TimestampVerficiationResponse | Record<string, never>>
// Other utilities or operations (like OpSHA256, serialization) // Other utilities or operations (like OpSHA256, serialization)
Ops: { Ops: {
@ -25,5 +29,9 @@ interface OpenTimestamps {
deserialize(bytes: Uint8Array): any deserialize(bytes: Uint8Array): any
// Other potential methods based on repo functions // Other potential methods based on repo functions
upgrade(file: any): Promise<void> upgrade(file: any): Promise<boolean>
}
interface TimestampVerficiationResponse {
bitcoin: { timestamp: number; height: number }
} }

View File

@ -1,6 +1,7 @@
export interface ProfileMetadata { export interface ProfileMetadata {
name?: string name?: string
display_name?: string display_name?: string
/** @deprecated use name instead */
username?: string username?: string
picture?: string picture?: string
banner?: string banner?: string

View File

@ -3,26 +3,26 @@ import { hexToNpub } from './nostr.ts'
import { Meta, SignedEventContent } from '../types' import { Meta, SignedEventContent } from '../types'
import { Event } from 'nostr-tools' import { Event } from 'nostr-tools'
import { EMPTY } from './const.ts' import { EMPTY } from './const.ts'
import { MarkType } from '../types/drawing.ts' import { DrawTool, MarkType } from '../types/drawing.ts'
import { import {
faT, faT,
faSignature, faSignature,
faBriefcase, faBriefcase,
faIdCard, faIdCard,
faHeading,
faClock, faClock,
faCalendarDays,
fa1, fa1,
faImage, faCalendarDays,
faSquareCheck,
faCheckDouble, faCheckDouble,
faPaperclip,
faCircleDot, faCircleDot,
faSquareCaretDown,
faTableCellsLarge,
faStamp,
faCreditCard, faCreditCard,
faPhone faHeading,
faImage,
faPaperclip,
faPhone,
faSquareCaretDown,
faSquareCheck,
faStamp,
faTableCellsLarge
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
/** /**
@ -152,114 +152,113 @@ const findOtherUserMarks = (marks: Mark[], pubkey: string): Mark[] => {
return marks.filter((mark) => mark.npub !== hexToNpub(pubkey)) return marks.filter((mark) => mark.npub !== hexToNpub(pubkey))
} }
export const DEFAULT_TOOLBOX = [ export const DEFAULT_TOOLBOX: DrawTool[] = [
{ {
identifier: MarkType.TEXT, identifier: MarkType.FULLNAME,
icon: faT, icon: faIdCard,
label: 'Text', label: 'Full Name',
active: true isComingSoon: true
},
{
identifier: MarkType.SIGNATURE,
icon: faSignature,
label: 'Signature',
active: false
}, },
{ {
identifier: MarkType.JOBTITLE, identifier: MarkType.JOBTITLE,
icon: faBriefcase, icon: faBriefcase,
label: 'Job Title', label: 'Job Title',
active: false isComingSoon: true
}, },
{ {
identifier: MarkType.FULLNAME, identifier: MarkType.SIGNATURE,
icon: faIdCard, icon: faSignature,
label: 'Full Name', label: 'Signature',
active: false isComingSoon: true
},
{
identifier: MarkType.INITIALS,
icon: faHeading,
label: 'Initials',
active: false
}, },
{ {
identifier: MarkType.DATETIME, identifier: MarkType.DATETIME,
icon: faClock, icon: faClock,
label: 'Date Time', label: 'Date Time',
active: false isComingSoon: true
}, },
{ {
identifier: MarkType.DATE, identifier: MarkType.TEXT,
icon: faCalendarDays, icon: faT,
label: 'Date', label: 'Text'
active: false
}, },
{ {
identifier: MarkType.NUMBER, identifier: MarkType.NUMBER,
icon: fa1, icon: fa1,
label: 'Number', label: 'Number',
active: false isComingSoon: true
},
{
identifier: MarkType.INITIALS,
icon: faHeading,
label: 'Initials',
isHidden: true
},
{
identifier: MarkType.DATE,
icon: faCalendarDays,
label: 'Date',
isHidden: true
}, },
{ {
identifier: MarkType.IMAGES, identifier: MarkType.IMAGES,
icon: faImage, icon: faImage,
label: 'Images', label: 'Images',
active: false isHidden: true
}, },
{ {
identifier: MarkType.CHECKBOX, identifier: MarkType.CHECKBOX,
icon: faSquareCheck, icon: faSquareCheck,
label: 'Checkbox', label: 'Checkbox',
active: false isHidden: true
}, },
{ {
identifier: MarkType.MULTIPLE, identifier: MarkType.MULTIPLE,
icon: faCheckDouble, icon: faCheckDouble,
label: 'Multiple', label: 'Multiple',
active: false isHidden: true
}, },
{ {
identifier: MarkType.FILE, identifier: MarkType.FILE,
icon: faPaperclip, icon: faPaperclip,
label: 'File', label: 'File',
active: false isHidden: true
}, },
{ {
identifier: MarkType.RADIO, identifier: MarkType.RADIO,
icon: faCircleDot, icon: faCircleDot,
label: 'Radio', label: 'Radio',
active: false isHidden: true
}, },
{ {
identifier: MarkType.SELECT, identifier: MarkType.SELECT,
icon: faSquareCaretDown, icon: faSquareCaretDown,
label: 'Select', label: 'Select',
active: false isHidden: true
}, },
{ {
identifier: MarkType.CELLS, identifier: MarkType.CELLS,
icon: faTableCellsLarge, icon: faTableCellsLarge,
label: 'Cells', label: 'Cells',
active: false isHidden: true
}, },
{ {
identifier: MarkType.STAMP, identifier: MarkType.STAMP,
icon: faStamp, icon: faStamp,
label: 'Stamp', label: 'Stamp',
active: false isHidden: true
}, },
{ {
identifier: MarkType.PAYMENT, identifier: MarkType.PAYMENT,
icon: faCreditCard, icon: faCreditCard,
label: 'Payment', label: 'Payment',
active: false isHidden: true
}, },
{ {
identifier: MarkType.PHONE, identifier: MarkType.PHONE,
icon: faPhone, icon: faPhone,
label: 'Phone', label: 'Phone',
active: false isHidden: true
} }
] ]

View File

@ -1,6 +1,6 @@
import { bytesToHex, hexToBytes } from '@noble/hashes/utils' import { bytesToHex, hexToBytes } from '@noble/hashes/utils'
import axios from 'axios' import axios from 'axios'
import _ from 'lodash' import _, { truncate } from 'lodash'
import { import {
Event, Event,
EventTemplate, EventTemplate,
@ -30,7 +30,7 @@ import {
import { AuthState, Keys } from '../store/auth/types' import { AuthState, Keys } from '../store/auth/types'
import { RelaysState } from '../store/relays/types' import { RelaysState } from '../store/relays/types'
import store from '../store/store' import store from '../store/store'
import { Meta, SignedEvent, UserAppData } from '../types' import { Meta, ProfileMetadata, SignedEvent, UserAppData } from '../types'
import { getDefaultRelayMap } from './relays' import { getDefaultRelayMap } from './relays'
import { parseJson, removeLeadingSlash } from './string' import { parseJson, removeLeadingSlash } from './string'
import { timeout } from './utils' import { timeout } from './utils'
@ -974,3 +974,16 @@ export const sendNotification = async (receiver: string, meta: Meta) => {
throw err throw err
}) })
} }
/**
* Show user's name, first available in order: display_name, name, or npub as fallback
* @param npub User identifier, it can be either pubkey or npub1 (we only show npub)
* @param profile User profile
*/
export const getProfileUsername = (
npub: `npub1${string}` | string,
profile?: ProfileMetadata
) =>
truncate(profile?.display_name || profile?.name || hexToNpub(npub), {
length: 16
})

View File

@ -1,7 +1,8 @@
import { Timestamp } from '../types' import { OpenTimestamp, OpenTimestampUpgradeVerifyResponse } from '../types'
import { retry } from './retry.ts' import { retry } from './retry.ts'
import { bytesToHex } from '@noble/hashes/utils' import { bytesToHex } from '@noble/hashes/utils'
import { utf8Encoder } from 'nostr-tools/utils' import { utf8Encoder } from 'nostr-tools/utils'
import { hexStringToUint8Array } from './string.ts'
/** /**
* Generates a timestamp for the provided nostr event ID. * Generates a timestamp for the provided nostr event ID.
@ -9,10 +10,10 @@ import { utf8Encoder } from 'nostr-tools/utils'
*/ */
export const generateTimestamp = async ( export const generateTimestamp = async (
nostrId: string nostrId: string
): Promise<Timestamp | undefined> => { ): Promise<OpenTimestamp | undefined> => {
try { try {
return { return {
timestamp: await retry(() => timestamp(nostrId)), value: await retry(() => timestamp(nostrId)),
nostrId: nostrId nostrId: nostrId
} }
} catch (error) { } catch (error) {
@ -21,6 +22,89 @@ export const generateTimestamp = async (
} }
} }
/**
* 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
*/
const upgrade = async (
t: OpenTimestamp
): Promise<OpenTimestampUpgradeVerifyResponse> => {
console.log('timestamp: ', t)
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 timestamp = async (nostrId: string): Promise<string> => {
const detachedTimestamp = const detachedTimestamp =
window.OpenTimestamps.DetachedTimestampFile.fromBytes( window.OpenTimestamps.DetachedTimestampFile.fromBytes(