Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions example-apps/dashproof-lab/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ function App() {
contractId={session.contractId}
onLoginPrompt={() => setLoginOpen(true)}
onAnchored={() => setRefreshKey((value) => value + 1)}
onViewChainHistory={openHistoryForChain}
/>
)}
{tab === "verify" && (
Expand Down
124 changes: 102 additions & 22 deletions example-apps/dashproof-lab/src/components/AnchorForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,14 @@ import {

import { createAnchor } from "../dash/createAnchor";
import { errorMessage } from "../dash/logger";
import { findAnchorByHash, type AnchorRecord } from "../dash/queries";
import { suggestChainId } from "../lib/chainId";
import { formatBytes, formatHashBlocks, formatTimestamp } from "../lib/format";
import {
formatBytes,
formatHashBlocks,
formatTimestamp,
truncateId,
} from "../lib/format";
import { bytesToHex, hashFile } from "../lib/hash";
import { useSession } from "../session/useSession";
import { OperationResultNotice } from "./OperationResultNotice";
Expand All @@ -21,12 +27,14 @@ interface AnchorFormProps {
contractId: string | null;
onAnchored: () => void;
onLoginPrompt: () => void;
onViewChainHistory?: (chainId: string) => void;
}

export function AnchorForm({
contractId,
onAnchored,
onLoginPrompt,
onViewChainHistory,
}: AnchorFormProps) {
const session = useSession();
const [selectedFile, setSelectedFile] = useState<File | null>(null);
Expand All @@ -43,6 +51,11 @@ export function AnchorForm({
const [hashCopied, setHashCopied] = useState(false);
const [submitting, setSubmitting] = useState(false);
const [hashing, setHashing] = useState(false);
const [checkingDuplicate, setCheckingDuplicate] = useState(false);
const [existingAnchor, setExistingAnchor] = useState<AnchorRecord | null>(
null,
);
const [anchoredCurrentFile, setAnchoredCurrentFile] = useState(false);
const [dragActive, setDragActive] = useState(false);
const inputId = "anchor-file-input";
const inputRef = useRef<HTMLInputElement | null>(null);
Expand All @@ -68,6 +81,9 @@ export function AnchorForm({
setEntryHash(null);
setHashHex("");
setHashCopied(false);
setCheckingDuplicate(false);
setExistingAnchor(null);
setAnchoredCurrentFile(false);
if (!file) {
if (chainIdAutoManagedRef.current) setChainId("");
return;
Expand All @@ -90,11 +106,39 @@ export function AnchorForm({
}
setStatusTone("info");
setStatusText("SHA-256 computed locally in the browser.");
setHashing(false);

if (!session.sdk || !contractId) return;

setCheckingDuplicate(true);
try {
const match = await findAnchorByHash({
sdk: session.sdk,
contractId,
entryHash: digest,
log: session.log,
});
if (fileSelectionRef.current !== requestId) return;
setExistingAnchor(match);
} catch (err) {
// The unique hash index is the real duplicate guard; a failed
// pre-flight lookup must not block the local hash preview.
if (fileSelectionRef.current === requestId) {
session.log?.(errorMessage(err), "error");
}
} finally {
if (fileSelectionRef.current === requestId) {
setCheckingDuplicate(false);
}
}
} catch (err) {
if (fileSelectionRef.current !== requestId) return;
setStatusTone("error");
setStatusText(errorMessage(err));
} finally {
setHashing(false);
if (fileSelectionRef.current === requestId) {
setHashing(false);
}
}
}

Expand Down Expand Up @@ -140,39 +184,58 @@ export function AnchorForm({
setHashCopied(true);
}

async function handleSubmit(event: FormEvent) {
event.preventDefault();
function getSubmitPayload() {
if (
session.status !== "authenticated" ||
!session.sdk ||
!session.keyManager ||
!selectedFile ||
!entryHash ||
!contractId
!contractId ||
chainId.trim().length === 0 ||
checkingDuplicate ||
existingAnchor ||
anchoredCurrentFile
) {
return;
return null;
}

return {
sdk: session.sdk,
keyManager: session.keyManager,
selectedFile,
entryHash,
contractId,
};
}

async function handleSubmit(event: FormEvent) {
event.preventDefault();
const submitPayload = getSubmitPayload();
if (!submitPayload) return;

setSubmitting(true);
setStatusText(null);
try {
await createAnchor({
sdk: session.sdk,
keyManager: session.keyManager,
contractId,
sdk: submitPayload.sdk,
keyManager: submitPayload.keyManager,
contractId: submitPayload.contractId,
log: session.log,
anchor: {
entryHash,
entryHash: submitPayload.entryHash,
chainId,
filename: selectedFile.name,
mimeType: selectedFile.type,
size: selectedFile.size,
filename: submitPayload.selectedFile.name,
mimeType: submitPayload.selectedFile.type,
size: submitPayload.selectedFile.size,
note,
},
});
setStatusTone("success");
setStatusText(
"Proof created and anchored on Dash Platform. Duplicate hashes are rejected by design.",
);
setAnchoredCurrentFile(true);
onAnchored();
} catch (err) {
setStatusTone("error");
Expand All @@ -182,14 +245,7 @@ export function AnchorForm({
}
}

const canSubmit =
session.status === "authenticated" &&
!!session.sdk &&
!!session.keyManager &&
!!selectedFile &&
!!entryHash &&
!!contractId &&
chainId.trim().length > 0;
const canSubmit = getSubmitPayload() !== null;

return (
<section className="mx-auto max-w-[1000px] rounded-lg border border-line bg-surface p-5">
Expand Down Expand Up @@ -314,6 +370,11 @@ export function AnchorForm({
<span className="h-4 w-4 animate-spin rounded-full border-2 border-line border-t-accent" />
<span>Computing hash…</span>
</div>
) : checkingDuplicate ? (
<div className="mt-3 flex items-center gap-3 text-[13px] text-ink">
<span className="h-4 w-4 animate-spin rounded-full border-2 border-line border-t-accent" />
<span>Checking for existing proof…</span>
</div>
) : (
<>
<div className="mt-3 whitespace-pre-wrap font-mono text-[11px] leading-6 text-ink">
Expand All @@ -338,13 +399,32 @@ export function AnchorForm({
/>
</label>

{existingAnchor && (
<OperationResultNotice tone="error" title="Already anchored">
This file already has a proof on Dash Platform (chain{" "}
<button
type="button"
onClick={() => onViewChainHistory?.(existingAnchor.chainId)}
className="font-medium underline underline-offset-2 transition hover:opacity-80"
>
{existingAnchor.chainId}
</button>
, anchored {formatTimestamp(existingAnchor.createdAt)} by{" "}
<span className="font-mono">
{truncateId(existingAnchor.ownerId, 12)}
</span>
). The hash index rejects duplicates, so a new proof can&rsquo;t be
created for it.
</OperationResultNotice>
)}

{statusText && (
<OperationResultNotice tone={statusTone} title="Proof status">
{statusText}
</OperationResultNotice>
)}

{session.status !== "authenticated" ? (
{existingAnchor ? null : session.status !== "authenticated" ? (
<div className="rounded-lg border border-dashed border-line px-4 py-4">
<div className="text-[13px] font-semibold text-ink">
Login required for submission
Expand Down
24 changes: 22 additions & 2 deletions example-apps/dashproof-lab/src/dash/createAnchor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import { Document } from "@dashevo/evo-sdk";

import { bytesToBase64, bytesToDocumentArray } from "../lib/hash";
import type { Logger } from "./logger";
import { errorMessage, type Logger } from "./logger";
import type { DashKeyManager, DashSdk } from "./types";

export interface CreateAnchorInput {
Expand All @@ -27,6 +27,18 @@ export interface CreateAnchorParams {
log?: Logger;
}

export const DUPLICATE_ANCHOR_MESSAGE =
"This file is already anchored. The proof contract rejects duplicate SHA-256 hashes.";

function isDuplicateAnchorError(err: unknown): boolean {
const message = errorMessage(err).toLowerCase();
return (
message.includes("duplicate") ||
message.includes("unique") ||
message.includes("already exists")
);
}

export async function createAnchor({
sdk,
keyManager,
Expand Down Expand Up @@ -73,6 +85,14 @@ export async function createAnchor({
dataContractId: contractId,
ownerId: identity.id,
});
await sdk.documents.create({ document, identityKey, signer });
try {
await sdk.documents.create({ document, identityKey, signer });
} catch (err) {
if (isDuplicateAnchorError(err)) {
throw new Error(DUPLICATE_ANCHOR_MESSAGE);
}
log?.(`Anchor submission failed: ${errorMessage(err)}`, "error", { err });
throw err;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
log?.("Proof anchor submitted.", "success");
}
6 changes: 5 additions & 1 deletion example-apps/dashproof-lab/src/dash/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@
*/
export type LogLevel = "info" | "success" | "error";

export type Logger = (message: string, level?: LogLevel) => void;
export type Logger = (
message: string,
level?: LogLevel,
details?: Record<string, unknown>,
) => void;

export function errorMessage(err: unknown): string {
if (err instanceof Error) return err.message;
Expand Down
8 changes: 6 additions & 2 deletions example-apps/dashproof-lab/src/session/SessionContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,14 @@ export function SessionProvider({ children }: { children: ReactNode }) {
loadStoredContractId(),
);

const log = useCallback<Logger>((message, level = "info") => {
const log = useCallback<Logger>((message, level = "info", details) => {
const method =
level === "error" ? "error" : level === "success" ? "info" : "log";
console[method](`[${level}] ${message}`);
if (details) {
console[method](`[${level}] ${message}`, details);
} else {
console[method](`[${level}] ${message}`);
}
if (level === "success") toast.success(message);
if (level === "error") toast.error(message);
}, []);
Expand Down
Loading