Files
nq/src/models.ts
T
2026-06-11 19:05:15 -07:00

164 lines
6.8 KiB
TypeScript

import { pubkey, signer, nip44EncryptToSelf } from "@welshman/app"
import type { Hex, QuorumMember } from "./protocol"
// ── Quorum & shard records ────────────────────────────────────────────────────
/** A known quorum — no secret material */
export type QuorumRecord = {
quorumPubkey: Hex
members: QuorumMember[]
threshold: number
/** Feldman commitments per participant index, from the last completed DKG or resharing */
commitments: Record<number, Hex[]>
/** Ordered list of kind 7057 inner event IDs (rotation history) */
rotationRecords: Hex[]
}
/** The user's secret share for one quorum, with the shard encrypted at rest */
export type ShardRecord = {
quorumPubkey: Hex
index: number
verificationShare: Hex
/** NIP-44 self-encrypted hex string of the shard bigint */
encryptedShard: string
}
// ── Display model ─────────────────────────────────────────────────────────────
export type QuorumStatusKind = "complete" | "sending" | "failed" | "pending"
/**
* A quorum as shown in the UI — either a completed quorum or a still-pending DKG
* invite. Derived from protocol events in the repository, so it appears as soon as
* the invite is created (optimistically) and persists across reloads, even if the
* invite was never delivered to any relay.
*/
export type DisplayedQuorum = {
/** Stable id used for selection: the quorum pubkey once complete, else the invite id */
id: string
/** Set once DKG completes */
quorumPubkey?: Hex
/** The kind 7050 invite id (always set; the origin of the quorum) */
inviteId: Hex
members: QuorumMember[]
threshold: number
complete: boolean
status: QuorumStatusKind
statusLabel: string
/** Members who have broadcast round 1 (the creator always counts) */
joined: number
declined: number
createdAt: number
}
// ── Session helpers ───────────────────────────────────────────────────────────
/** Proof-of-knowledge from a round-1 message */
export type PoKProof = { R: Hex; s: Hex }
/** Round-1 broadcast from one participant */
export type Round1Data = { commitments: Hex[]; proof: PoKProof }
// ── DKG session ───────────────────────────────────────────────────────────────
export type DkgPhase = "round1" | "round2" | "confirming" | "complete"
/** In-progress DKG session, keyed by the kind 7050 inner event ID */
export type DkgSession = {
inviteId: Hex
members: Hex[]
threshold: number
phase: DkgPhase
/** Own Feldman commitments (set once we broadcast round 1) */
myCommitments?: Hex[]
/** Own polynomial coefficients, NIP-44 self-encrypted */
myEncryptedPoly?: string
/** Round-1 data received, keyed by sender pubkey */
round1: Record<Hex, Round1Data>
/** Round-2 shares received, keyed by sender participant index */
round2: Record<number, Hex>
/** Members who declined, keyed by pubkey, value is their optional reason */
declines: Record<Hex, string>
/** Set when the session was aborted (equivocation or a crypto-verification failure) */
aborted?: boolean
/** True once we have locally finalized and saved this quorum (usable for signing) */
complete?: boolean
/** Count of matching kind 7053 confirmations received (ours + peers') */
confirmed?: number
}
// ── Resharing session ─────────────────────────────────────────────────────────
export type ResharingPhase = "round1" | "round2" | "confirming" | "complete"
/** In-progress resharing session, keyed by the kind 7054 inner event ID */
export type ResharingSession = {
proposalId: Hex
quorumPubkey: Hex
contributors: Hex[]
newMembers: Hex[]
newThreshold: number
phase: ResharingPhase
myCommitments?: Hex[]
myEncryptedPoly?: string
/** Round-1 data received, keyed by sender pubkey */
round1: Record<Hex, Round1Data>
/** Round-2 shares received, keyed by sender participant index */
round2: Record<number, Hex>
/** Members who declined, keyed by pubkey, value is their optional reason */
declines: Record<Hex, string>
}
// ── Signing session ───────────────────────────────────────────────────────────
export type SigningPhase = "round1" | "round2" | "complete"
/** In-progress signing session, keyed by the kind 7058 inner event ID */
export type SigningSession = {
requestId: Hex
quorumPubkey: Hex
/** Hex-encoded message bytes */
msgHex: Hex
phase: SigningPhase
signingSet: number[]
/** Own nonce commitments (present until we have broadcast our round-2 share) */
myNonces?: { D: Hex; E: Hex }
/** Round-1 nonce commitments from others, keyed by sender pubkey */
round1: Record<Hex, { D: Hex; E: Hex }>
/** Round-2 signature shares, keyed by participant index */
shares: Record<number, Hex>
/** Members who declined, keyed by pubkey, value is their optional reason */
declines: Record<Hex, string>
}
// ── Shard / polynomial encryption ────────────────────────────────────────────
// Shards and polynomials are self-encrypted via the active session's signer:
// NIP-44 encrypt(myPubkey, ...). The signer derives the conversation key from
// (myPrivkey, myPubkey), which is unique to the key and opaque to any other
// party. The signer store is typed non-optional but is undefined at runtime
// when there is no active session, hence the guards.
export async function encryptShard(shard: bigint): Promise<string> {
return nip44EncryptToSelf(shard.toString(16))
}
export async function decryptShard(encryptedShard: string): Promise<bigint> {
const $pubkey = pubkey.get()
const $signer = signer.get()
if (!$pubkey || !$signer) { throw new Error("Cannot decrypt without an active signer") }
const hex = await $signer.nip44.decrypt($pubkey, encryptedShard)
return BigInt("0x" + hex)
}
export async function encryptPoly(poly: bigint[]): Promise<string> {
return nip44EncryptToSelf(JSON.stringify(poly.map(c => c.toString(16))))
}
export async function decryptPoly(encryptedPoly: string): Promise<bigint[]> {
const $pubkey = pubkey.get()
const $signer = signer.get()
if (!$pubkey || !$signer) { throw new Error("Cannot decrypt without an active signer") }
const json = await $signer.nip44.decrypt($pubkey, encryptedPoly)
return (JSON.parse(json) as string[]).map(h => BigInt("0x" + h))
}