Skip to content

Commit 2bc0e24

Browse files
authored
Merge pull request #13 from kojibai/main
v2.5.0
2 parents 741c1a9 + 4e45a0e commit 2bc0e24

6 files changed

Lines changed: 120 additions & 32 deletions

File tree

src/SigilMarkets/utils/prophecySigil.ts

Lines changed: 27 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import type { ProphecyId, ProphecySigilPayloadV1, ProphecySigilZkBundle } from "
66
import { asProphecyId } from "../types/prophecySigilTypes";
77
import type { KaiPulse, MicroDecimalString, PhiMicro } from "../types/marketTypes";
88
import type { KaiMoment as KaiMomentExact } from "../../utils/kai_pulse";
9+
import { STEPS_BEAT } from "../../utils/kai_pulse";
10+
import { stepIndexFromPulse, stepProgressWithinStepFromPulse } from "../../utils/kaiMath";
911
import { asMicroDecimalString } from "../types/marketTypes";
1012
import type { KaiSignature, UserPhiKey } from "../types/vaultTypes";
1113
import { sha256Hex } from "./ids";
@@ -263,25 +265,31 @@ export const buildProphecyPayloadBase = (args: Readonly<{
263265
userPhiKey: UserPhiKey;
264266
kaiSignature: KaiSignature;
265267
moment: KaiMomentExact;
266-
}>): Omit<ProphecySigilPayloadV1, "canonicalHash" | "zk"> => ({
267-
v: "SM-PROPHECY-1",
268-
kind: "prophecy",
269-
prophecyId: args.prophecyId,
270-
text: args.text,
271-
textEnc: args.textEnc,
272-
category: args.category,
273-
expirationPulse: args.expirationPulse,
274-
escrowPhiMicro: args.escrowPhiMicro,
275-
evidence: args.evidence,
276-
userPhiKey: args.userPhiKey,
277-
kaiSignature: args.kaiSignature,
278-
pulse: args.moment.pulse,
279-
beat: args.moment.beat,
280-
stepIndex: args.moment.stepIndex,
281-
stepPct: args.moment.stepPctAcrossBeat,
282-
chakraDay: args.moment.chakraDay,
283-
createdAtPulse: args.moment.pulse,
284-
});
268+
}>): Omit<ProphecySigilPayloadV1, "canonicalHash" | "zk"> => {
269+
const stepIndex = stepIndexFromPulse(args.moment.pulse, STEPS_BEAT);
270+
const stepProgress = stepProgressWithinStepFromPulse(args.moment.pulse, STEPS_BEAT);
271+
const stepPct = (stepIndex + stepProgress) / STEPS_BEAT;
272+
273+
return {
274+
v: "SM-PROPHECY-1",
275+
kind: "prophecy",
276+
prophecyId: args.prophecyId,
277+
text: args.text,
278+
textEnc: args.textEnc,
279+
category: args.category,
280+
expirationPulse: args.expirationPulse,
281+
escrowPhiMicro: args.escrowPhiMicro,
282+
evidence: args.evidence,
283+
userPhiKey: args.userPhiKey,
284+
kaiSignature: args.kaiSignature,
285+
pulse: args.moment.pulse,
286+
beat: args.moment.beat,
287+
stepIndex,
288+
stepPct,
289+
chakraDay: args.moment.chakraDay,
290+
createdAtPulse: args.moment.pulse,
291+
};
292+
};
285293

286294
export const buildProphecySvg = (payload: ProphecySigilPayloadV1, textEncoded?: string): string => {
287295
const textEnc = payload.textEnc ?? "uri";

src/config/buildInfo.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
export const APP_NAME = "Vérahai";
2-
export const APP_VERSION = "2.3.0";
2+
export const APP_VERSION = "2.5.0";
33
export const GITHUB_REPO_URL = "https://github.com/phinetwork/verahai";
44

55
export const GITHUB_RELEASE_URL = (version: string): string =>

src/pages/SigilPage/SigilPage.tsx

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,12 @@ export default function SigilPage() {
475475
const candidate = raw as ProphecySigilPayloadV1;
476476
return candidate.kind === "prophecy" ? candidate : null;
477477
}, [payload]);
478+
const prophecyPayloadExtras = useMemo(
479+
() => (prophecyPayload ? { prophecyPayload } : undefined),
480+
[prophecyPayload],
481+
);
482+
const prophecyPhiKey = prophecyPayload?.userPhiKey ?? null;
483+
const prophecyKaiSignature = prophecyPayload?.kaiSignature ?? null;
478484

479485
const prophecyDetails = useMemo<ProphecyDetails | null>(() => {
480486
if (!prophecyPayload) return null;
@@ -1460,14 +1466,16 @@ const openHistoryPress = useFastPress<HTMLButtonElement>(() => setHistoryOpen(tr
14601466
const rawAmount = (meta as Partial<{ claimExtendAmount?: unknown }>).claimExtendAmount;
14611467
const amountForShare: number | null = typeof rawAmount === "number" ? rawAmount : null;
14621468

1469+
const resolvedUserPhiKey = meta.userPhiKey ?? prophecyPhiKey;
1470+
const resolvedKaiSignature = meta.kaiSignature ?? prophecyKaiSignature;
14631471
const shareable: ShareableSigilMeta = {
14641472
pulse: meta.pulse,
14651473
beat: meta.beat,
14661474
chakraDay: meta.chakraDay ?? "Root",
14671475
stepsPerBeat: stepsNum,
14681476
stepIndex: sealedStepIndex,
1469-
userPhiKey: meta.userPhiKey ?? null,
1470-
kaiSignature: meta.kaiSignature ?? null,
1477+
userPhiKey: resolvedUserPhiKey ?? null,
1478+
kaiSignature: resolvedKaiSignature ?? null,
14711479
canonicalHash: canonical,
14721480
transferNonce: meta.transferNonce ?? null,
14731481
expiresAtPulse: meta.expiresAtPulse ?? null,
@@ -1480,7 +1488,7 @@ const openHistoryPress = useFastPress<HTMLButtonElement>(() => setHistoryOpen(tr
14801488
routeHash,
14811489
stepsPerBeat: STEPS_PER_BEAT,
14821490
stepIndexFromPulse,
1483-
});
1491+
}, prophecyPayloadExtras);
14841492

14851493
let finalUrl = out?.url || `/s/${canonical}`;
14861494
try {
@@ -1506,7 +1514,7 @@ current.searchParams.forEach((v, k) => {
15061514
setSealOpen(true);
15071515
return finalUrl;
15081516
},
1509-
[localHash, routeHash]
1517+
[localHash, routeHash, prophecyKaiSignature, prophecyPayloadExtras, prophecyPhiKey]
15101518
);
15111519

15121520
/* 11-breath upgrade claim window helper */
@@ -1689,14 +1697,15 @@ current.searchParams.forEach((v, k) => {
16891697
stepIndex: payload.stepIndex ?? null,
16901698
exportedAtPulse: payload.exportedAtPulse ?? null,
16911699
canonicalHash: payload.canonicalHash ?? null,
1692-
userPhiKey: payload.userPhiKey ?? null,
1693-
kaiSignature: payload.kaiSignature ?? null,
1700+
userPhiKey: payload.userPhiKey ?? prophecyPhiKey ?? null,
1701+
kaiSignature: payload.kaiSignature ?? prophecyKaiSignature ?? null,
16941702
transferNonce: payload.transferNonce ?? null,
16951703
expiresAtPulse: payload.expiresAtPulse ?? null,
16961704
claimExtendUnit: unitForExport,
16971705
claimExtendAmount: amountForExport,
16981706
attachment: payload.attachment ?? null,
16991707
provenance: (payload.provenance as ProvenanceEntry[] | null) ?? null,
1708+
payloadExtras: prophecyPayloadExtras,
17001709
}
17011710
: null,
17021711
isFutureSealed,
@@ -2720,8 +2729,8 @@ return () => document.body.classList.remove(cls);
27202729
>
27212730
<KaiSigil
27222731
pulse={pulse}
2723-
beat={payload.beat}
2724-
stepIndex={typeof payload.stepIndex === "number" ? payload.stepIndex : undefined}
2732+
beat={beatIndexFromPulse(pulse)}
2733+
stepIndex={stepIndex}
27252734
chakraDay={chakraDay}
27262735
size={sigilSize}
27272736
hashMode="deterministic"

src/pages/SigilPage/exportZip.ts

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,14 @@ type ChakraDay =
2525
| "Third Eye"
2626
| "Crown";
2727

28+
type UnknownRecord = Record<string, unknown>;
29+
30+
const SVG_NS = "http://www.w3.org/2000/svg";
31+
const PROOF_METADATA_ID = "kai-voh-proof";
32+
33+
const isRecord = (value: unknown): value is UnknownRecord =>
34+
typeof value === "object" && value !== null && !Array.isArray(value);
35+
2836
function stableStringify(v: unknown): string {
2937
if (v === null || typeof v !== "object") return JSON.stringify(v);
3038
if (Array.isArray(v)) return "[" + v.map(stableStringify).join(",") + "]";
@@ -58,6 +66,7 @@ export type ExportableSigilMeta = {
5866
/** misc */
5967
attachment?: { name?: string | null } | null;
6068
provenance?: Array<Record<string, unknown>> | null;
69+
payloadExtras?: Record<string, unknown>;
6170
};
6271

6372
/** Coerce any free-form value into a valid ChakraDay; default to "Root". */
@@ -158,6 +167,47 @@ function updateSvgUrlSurfaces(svgEl: SVGSVGElement, fullUrl: string): void {
158167
});
159168
}
160169

170+
function buildProphecyZkBundle(payloadExtras?: Record<string, unknown>, shareUrl?: string): UnknownRecord | null {
171+
if (!payloadExtras) return null;
172+
const rawProphecy = isRecord(payloadExtras.prophecyPayload) ? payloadExtras.prophecyPayload : null;
173+
const rawZk = rawProphecy && isRecord(rawProphecy.zk) ? rawProphecy.zk : null;
174+
if (!rawZk) return null;
175+
176+
const zkPoseidonHash = typeof rawZk.poseidonHash === "string" ? rawZk.poseidonHash : undefined;
177+
const zkProof = "proof" in rawZk ? rawZk.proof : undefined;
178+
const zkPublicInputs =
179+
Array.isArray(rawZk.publicInputs) || typeof rawZk.publicInputs === "string"
180+
? rawZk.publicInputs
181+
: undefined;
182+
183+
if (!zkPoseidonHash && zkProof === undefined && zkPublicInputs === undefined) return null;
184+
185+
const bundle: UnknownRecord = {
186+
shareUrl,
187+
zkPoseidonHash,
188+
zkProof,
189+
zkPublicInputs,
190+
};
191+
192+
Object.keys(bundle).forEach((key) => {
193+
if (bundle[key] === undefined) delete bundle[key];
194+
});
195+
196+
return bundle;
197+
}
198+
199+
function upsertProofMetadata(svgEl: SVGSVGElement, bundle: UnknownRecord): void {
200+
const doc = svgEl.ownerDocument ?? document;
201+
let meta = svgEl.querySelector<SVGMetadataElement>(`metadata#${PROOF_METADATA_ID}`);
202+
if (!meta) {
203+
meta = doc.createElementNS(SVG_NS, "metadata") as SVGMetadataElement;
204+
meta.setAttribute("id", PROOF_METADATA_ID);
205+
meta.setAttribute("type", "application/json");
206+
svgEl.appendChild(meta);
207+
}
208+
meta.textContent = JSON.stringify(bundle);
209+
}
210+
161211
export async function exportZIP(ctx: {
162212
expired: boolean;
163213
exporting: boolean;
@@ -240,6 +290,7 @@ export async function exportZIP(ctx: {
240290
claimExtendUnit: payload.claimExtendUnit ?? expiryUnit,
241291
claimExtendAmount: payload.claimExtendAmount ?? expiryAmount,
242292
canonicalHash: (localHash || payload.canonicalHash || routeHash || null)?.toString() ?? null,
293+
payloadExtras: payload.payloadExtras,
243294
};
244295

245296
// canonical Σ and Φ (0-based stepIndex)
@@ -258,8 +309,11 @@ export async function exportZIP(ctx: {
258309
const claimedMetaCanon: ExportableSigilMeta = {
259310
...claimedMeta,
260311
kaiSignature: canonicalSig,
261-
userPhiKey: claimedMeta.userPhiKey || phiKeyCanon,
312+
userPhiKey: phiKeyCanon,
262313
};
314+
svgEl.setAttribute("data-kai-signature", canonicalSig);
315+
svgEl.setAttribute("data-phi-key", phiKeyCanon);
316+
const payloadExtras = claimedMetaCanon.payloadExtras ?? {};
263317

264318
// Build the canonical share URL for manifest — canonical is in the path, NOT the payload
265319
const canonicalLower = (localHash || routeHash || "").toLowerCase();
@@ -279,20 +333,29 @@ export async function exportZIP(ctx: {
279333

280334
const fullUrlForManifest = rewriteUrlPayload(
281335
baseUrlForManifest,
282-
sharePayloadForManifest,
336+
{ ...payloadExtras, ...sharePayloadForManifest },
283337
tokenForManifest
284338
);
285339

286340
// Canonical payload write only (single call) — include URL hints for readers
287341
const { putMetadata } = await import("../../utils/svgMeta");
288342
const metaForSvg: Record<string, unknown> = {
343+
...payloadExtras,
289344
...claimedMetaCanon,
290345
stepsPerBeat: stepsNum,
291346
shareUrl: fullUrlForManifest, // hint for consumers
292347
fullUrl: fullUrlForManifest, // alias
293348
};
349+
if (typeof metaForSvg.userPhiKey === "string" && !metaForSvg.phiKey) {
350+
metaForSvg.phiKey = metaForSvg.userPhiKey;
351+
}
294352
putMetadata(svgEl, metaForSvg);
295353

354+
const zkBundle = buildProphecyZkBundle(payloadExtras, fullUrlForManifest);
355+
if (zkBundle) {
356+
upsertProofMetadata(svgEl, zkBundle);
357+
}
358+
296359
// Display-only exposure (non-canonical marker)
297360
try {
298361
svgEl.setAttribute("data-step-index", String(sealedStepIndex));
@@ -383,6 +446,7 @@ export async function exportZIP(ctx: {
383446
fullUrl: fullUrlForManifest,
384447
p: pValue,
385448
urlQuery: { p: pValue, t: tValue },
449+
payloadExtras: Object.keys(payloadExtras).length ? payloadExtras : null,
386450
};
387451
const manifestHash = await sha256HexCanon(stableStringify(manifestPayload));
388452
const manifest = { ...manifestPayload, manifestHash };

src/pages/SigilPage/linkShare.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,8 @@ function resolveStepIndex(
122122
export function shareTransferLink(
123123
meta: ShareableSigilMeta,
124124
forcedToken: string | undefined,
125-
deps: ShareDeps
125+
deps: ShareDeps,
126+
payloadExtras?: Record<string, unknown>
126127
): { url: string; token: string } | null {
127128
// Canonical hash goes in the path, not in the payload
128129
const canonical = ((meta.canonicalHash ?? deps.localHash ?? deps.routeHash) ?? "").toLowerCase();
@@ -161,7 +162,10 @@ export function shareTransferLink(
161162
claimExtendAmount: meta.claimExtendAmount ?? undefined,
162163
};
163164

164-
const url = ensureClaimTimeInUrl(withToken, metaForP as SigilPayload);
165+
const url = ensureClaimTimeInUrl(withToken, {
166+
...(payloadExtras ?? {}),
167+
...metaForP,
168+
} as SigilPayload);
165169
return { url, token };
166170
}
167171

src/utils/payload.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,9 @@ export function decodePayloadFromQuery(search: string): SigilPayload | null {
253253
if (rawProphecy.evidence && typeof rawProphecy.evidence === "object") {
254254
trimmedProphecy.evidence = rawProphecy.evidence;
255255
}
256+
if (rawProphecy.zk && typeof rawProphecy.zk === "object") {
257+
trimmedProphecy.zk = rawProphecy.zk;
258+
}
256259
(payload as SigilPayload & { prophecyPayload?: unknown }).prophecyPayload =
257260
trimmedProphecy;
258261
}

0 commit comments

Comments
 (0)