diff --git a/src/services/index.ts b/src/services/index.ts index 79b5128..b8d275a 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -1 +1,2 @@ export * from './cache' +export * from './signer' diff --git a/src/services/signer/index.ts b/src/services/signer/index.ts new file mode 100644 index 0000000..8851028 --- /dev/null +++ b/src/services/signer/index.ts @@ -0,0 +1,143 @@ +import { toast } from 'react-toastify' +import { Meta, SignedEventContent } from '../../types' +import { + parseCreateSignatureEventContent, + parseNostrEvent, + SigitStatus, + SignStatus +} from '../../utils' +import { MetaParseError } from '../../types/errors/MetaParseError' +import { verifyEvent } from 'nostr-tools' +import { appPrivateRoutes, appPublicRoutes } from '../../routes' + +export class SignerService { + #signers: `npub1${string}`[] = [] + #nextSigner: `npub1${string}` | undefined + #signatures = new Map<`npub1${string}`, string>() + #signersStatus = new Map<`npub1${string}`, SignStatus>() + #lastSignerSig: string | undefined + constructor(source: Meta) { + this.#process(source.createSignature, source.docSignatures) + } + + getNextSigner = () => { + return this.#nextSigner + } + + isNextSigner = (npub: `npub1${string}`) => { + return this.#nextSigner === npub + } + + isLastSigner = (npub: `npub1${string}`) => { + const lastIndex = this.#signers.length - 1 + const npubIndex = this.#signers.indexOf(npub) + return npubIndex === lastIndex + } + + #isFullySigned = () => { + const signedBy = Object.keys(this.#signatures) as `npub1${string}`[] + const isCompletelySigned = this.#signers.every((signer) => + signedBy.includes(signer) + ) + return isCompletelySigned + } + + getSignedStatus = () => { + return this.#isFullySigned() ? SigitStatus.Complete : SigitStatus.Partial + } + + getSignerStatus = (npub: `npub1${string}`) => { + return this.#signersStatus.get(npub) + } + + getNavigate = (npub: `npub1${string}`) => { + return this.isNextSigner(npub) + ? appPrivateRoutes.sign + : appPublicRoutes.verify + } + + getLastSignerSig = () => { + return this.#lastSignerSig + } + + #process = ( + createSignature: string, + docSignatures: { [key: `npub1${string}`]: string } + ) => { + try { + const createSignatureEvent = parseNostrEvent(createSignature) + const { signers } = parseCreateSignatureEventContent( + createSignatureEvent.content + ) + const getPrevSignerSig = (npub: `npub1${string}`) => { + if (signers[0] === npub) { + return createSignatureEvent.sig + } + + // Find the index of signer + const currentSignerIndex = signers.findIndex( + (signer) => signer === npub + ) + + // Return if could not found user in signer's list + if (currentSignerIndex === -1) return + + // Find prev signer + const prevSigner = signers[currentSignerIndex - 1] + + // Get the signature of prev signer + return this.#signatures.get(prevSigner) + } + + this.#signers = [...signers] + for (const npub in docSignatures) { + try { + // Parse each signature event + const event = parseNostrEvent(docSignatures[npub as `npub1${string}`]) + this.#signatures.set(npub as `npub1${string}`, event.sig) + const isValidSignature = verifyEvent(event) + if (isValidSignature) { + const prevSignersSig = getPrevSignerSig(npub as `npub1${string}`) + const signedEvent: SignedEventContent = JSON.parse(event.content) + if ( + signedEvent.prevSig && + prevSignersSig && + signedEvent.prevSig === prevSignersSig + ) { + this.#signersStatus.set( + npub as `npub1${string}`, + SignStatus.Signed + ) + this.#lastSignerSig = event.sig + } + } else { + this.#signersStatus.set( + npub as `npub1${string}`, + SignStatus.Invalid + ) + } + } catch (error) { + this.#signersStatus.set(npub as `npub1${string}`, SignStatus.Invalid) + } + } + + this.#signers + .filter((s) => !this.#signatures.has(s)) + .forEach((s) => this.#signersStatus.set(s, SignStatus.Pending)) + + // Get the first signer that hasn't signed + const nextSigner = this.#signers.find((s) => !this.#signatures.has(s)) + if (nextSigner) { + this.#nextSigner = nextSigner + this.#signersStatus.set(nextSigner, SignStatus.Awaiting) + } + } catch (error) { + if (error instanceof MetaParseError) { + toast.error(error.message) + console.error(error.name, error.message, error.cause, error.context) + } else { + console.error('Unexpected error', error) + } + } + } +}