Added Errors to Notarization Progress (#147)

* feat: added Error for RequestProgress

* feat: added logic to handle errors depending on progress

* chore: linting errors

* fix: linting build error

* Avoid magic numbers

* lint

* feat: alpha.8

* fix: package.json path

* chore: update lockfiles

* fix: notary url

* feat: notarization timeouts

* fix: notarization timeouts

* feat: fixed comments + errors are properly thrown and status changes

* fix: notarization timeouts work properly now

* fix: timeout error message fix

* fix: lint

* fix: lint

* fix: errors should keep state after being thrown now

* chore: lint

* fix: fixing metadata with new progress status

---------

Co-authored-by: Hendrik Eeckhaut <hendrik@eeckhaut.org>
Co-authored-by: tsukino <0xtsukino@gmail.com>
This commit is contained in:
Tanner
2025-04-15 10:00:57 -07:00
committed by GitHub
parent 3f37c1aee8
commit ade6d7e575
7 changed files with 249 additions and 123 deletions

24
package-lock.json generated
View File

@@ -3603,9 +3603,9 @@
}
},
"node_modules/@pmmmwh/react-refresh-webpack-plugin": {
"version": "0.5.15",
"resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.15.tgz",
"integrity": "sha512-LFWllMA55pzB9D34w/wXUCf8+c+IYKuJDgxiZ3qMhl64KRMBHYM1I3VdGaD2BV5FNPV2/S2596bppxHbv2ZydQ==",
"version": "0.5.16",
"resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.16.tgz",
"integrity": "sha512-kLQc9xz6QIqd2oIYyXRUiAp79kGpFBm3fEM9ahfG1HI0WI5gdZ2OVHWdmZYnwODt7ISck+QuQ6sBPrtvUBML7Q==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -8710,9 +8710,9 @@
}
},
"node_modules/html-entities": {
"version": "2.5.3",
"resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.5.3.tgz",
"integrity": "sha512-D3AfvN7SjhTgBSA8L1BN4FpPzuEd06uy4lHwSoRWr0lndi9BKaNzPLKGOWZ2ocSGguozr08TTb2jhCLHaemruw==",
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz",
"integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==",
"dev": true,
"funding": [
{
@@ -11735,9 +11735,9 @@
}
},
"node_modules/postcss-load-config/node_modules/yaml": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz",
"integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==",
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz",
"integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==",
"license": "ISC",
"bin": {
"yaml": "bin.mjs"
@@ -14744,9 +14744,9 @@
}
},
"node_modules/use-sync-external-store": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz",
"integrity": "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==",
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz",
"integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"

View File

@@ -115,14 +115,15 @@ export async function setNotaryRequestError(
export async function setNotaryRequestProgress(
id: string,
progress: RequestProgress,
errorMessage?: string,
): Promise<RequestHistory | null> {
const existing = await historyDb.get(id);
if (!existing) return null;
const newReq: RequestHistory = {
...existing,
progress,
errorMessage,
};
await historyDb.put(id, newReq);

View File

@@ -167,9 +167,13 @@ export enum RequestProgress {
SendingRequest,
ReadingTranscript,
FinalizingOutputs,
Error,
}
export function progressText(progress: RequestProgress): string {
export function progressText(
progress: RequestProgress,
errorMessage?: string,
): string {
switch (progress) {
case RequestProgress.CreatingProver:
return 'Creating prover...';
@@ -183,6 +187,8 @@ export function progressText(progress: RequestProgress): string {
return 'Reading request transcript...';
case RequestProgress.FinalizingOutputs:
return 'Finalizing notarization outputs...';
case RequestProgress.Error:
return errorMessage ? errorMessage : 'Error: Notarization Failed';
}
}
@@ -210,6 +216,7 @@ export type RequestHistory = {
secretHeaders?: string[];
secretResps?: string[];
cid?: string;
errorMessage?: string;
metadata?: {
[k: string]: string;
};
@@ -430,9 +437,9 @@ async function handleUpdateRequestProgress(
request: BackgroundAction,
sendResponse: (data?: any) => void,
) {
const { id, progress } = request.data;
const { id, progress, errorMessage } = request.data;
const newReq = await setNotaryRequestProgress(id, progress);
const newReq = await setNotaryRequestProgress(id, progress, errorMessage);
if (!newReq) return;
await pushToRedux(addRequestHistory(await getNotaryRequest(id)));
@@ -553,69 +560,82 @@ async function runPluginProver(request: BackgroundAction, now = Date.now()) {
});
await setNotaryRequestStatus(id, 'pending');
await pushToRedux(addRequestHistory(await getNotaryRequest(id)));
const onProverResponse = async (request: any) => {
const { data, type } = request;
let listenerActive = true;
let responseListener: (request: any) => void;
if (type !== OffscreenActionTypes.create_prover_response) {
return;
}
const proverPromise = new Promise<void>((resolve, reject) => {
responseListener = async (request: any) => {
if (!listenerActive) return;
if (data.error) {
console.error(data.error);
return;
}
const { data, type } = request;
if (data.id !== id) {
return;
}
if (type !== OffscreenActionTypes.create_prover_response) {
return;
}
const transcript: { recv: number[]; sent: number[] } = data.transcript;
if (data.id !== id) {
return;
}
const { body: recvBody } = parseHttpMessage(
Buffer.from(transcript.recv),
'response',
);
try {
if (data.error) {
throw new Error(data.error);
}
if (getSecretResponse) {
secretResps = await getSecretResponseFn(
...recvBody.map((body) => body.toString('utf-8')),
);
}
const transcript: { recv: number[]; sent: number[] } = data.transcript;
const commit = {
sent: subtractRanges(
{ start: 0, end: transcript.sent.length },
mapStringToRange(
secretHeaders,
Buffer.from(transcript.sent).toString('utf-8'),
),
),
recv: subtractRanges(
{ start: 0, end: transcript.recv.length },
mapStringToRange(
secretResps,
Buffer.from(transcript.recv).toString('utf-8'),
),
),
const { body: recvBody } = parseHttpMessage(
Buffer.from(transcript.recv),
'response',
);
if (getSecretResponse) {
secretResps = await getSecretResponseFn(
...recvBody.map((body) => body.toString('utf-8')),
);
}
const commit = {
sent: subtractRanges(
{ start: 0, end: transcript.sent.length },
mapStringToRange(
secretHeaders,
Buffer.from(transcript.sent).toString('utf-8'),
),
),
recv: subtractRanges(
{ start: 0, end: transcript.recv.length },
mapStringToRange(
secretResps,
Buffer.from(transcript.recv).toString('utf-8'),
),
),
};
browser.runtime.sendMessage({
type: OffscreenActionTypes.create_presentation_request,
data: {
id,
commit,
notaryUrl,
websocketProxyUrl,
},
});
resolve();
} catch (error) {
console.error('Prover response error:', error);
reject(error);
} finally {
listenerActive = false;
browser.runtime.onMessage.removeListener(responseListener);
}
};
browser.runtime.sendMessage({
type: OffscreenActionTypes.create_presentation_request,
data: {
id,
commit,
notaryUrl,
websocketProxyUrl,
},
});
browser.runtime.onMessage.removeListener(onProverResponse);
};
browser.runtime.onMessage.addListener(onProverResponse);
browser.runtime.onMessage.addListener(responseListener);
});
browser.runtime.sendMessage({
type: OffscreenActionTypes.create_prover_request,
@@ -631,6 +651,34 @@ async function runPluginProver(request: BackgroundAction, now = Date.now()) {
maxSentData,
},
});
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => {
if (listenerActive) {
listenerActive = false;
browser.runtime.onMessage.removeListener(responseListener);
reject(new Error('Notarization Timed Out'));
}
// 3 minute timeout
}, 180000);
});
try {
await Promise.race([proverPromise, timeoutPromise]);
} catch (error: any) {
await setNotaryRequestStatus(id, 'error');
await setNotaryRequestError(id, error.message);
browser.runtime.sendMessage({
type: BackgroundActiontype.update_request_progress,
data: {
id,
progress: RequestProgress.Error,
error: error.message,
},
});
await pushToRedux(addRequestHistory(await getNotaryRequest(id)));
throw error;
}
}
async function handleGetSecretsFromTranscript(

View File

@@ -21,6 +21,10 @@ import { OffscreenActionTypes } from './types';
import { PresentationJSON } from '../../utils/types';
import { verify } from 'tlsn-js-v5';
import { waitForEvent } from '../utils';
import {
setNotaryRequestError,
setNotaryRequestStatus,
} from '../Background/db';
const { init, Prover, Presentation, Verifier }: any = Comlink.wrap(
new Worker(new URL('./worker.ts', import.meta.url)),
@@ -122,7 +126,6 @@ export const onCreatePresentationRequest = async (request: any) => {
reveal: commit,
})) as TPresentation;
const json = await presentation.json();
browser.runtime.sendMessage({
type: BackgroundActiontype.finish_prove_request,
data: {
@@ -435,7 +438,6 @@ async function createProof(options: {
})) as TPresentation;
const json = await presentation.json();
return {
...json,
meta: {
@@ -473,30 +475,51 @@ async function createProver(options: {
const hostname = urlify(url)?.hostname || '';
const notary = NotaryServer.from(notaryUrl);
try {
const prover: TProver = await handleProgress(
id,
RequestProgress.CreatingProver,
() =>
new Prover({
id,
serverDns: hostname,
maxSentData,
maxRecvData,
}),
'Error creating prover',
);
updateRequestProgress(id, RequestProgress.CreatingProver);
const prover: TProver = await new Prover({
id,
serverDns: hostname,
maxSentData,
maxRecvData,
});
const sessionUrl = await handleProgress(
id,
RequestProgress.GettingSession,
() => notary.sessionUrl(maxSentData, maxRecvData),
'Error getting session from Notary',
);
updateRequestProgress(id, RequestProgress.GettingSession);
const sessionUrl = await notary.sessionUrl(maxSentData, maxRecvData);
await handleProgress(
id,
RequestProgress.SettingUpProver,
() => prover.setup(sessionUrl),
'Error setting up prover',
);
updateRequestProgress(id, RequestProgress.SettingUpProver);
await prover.setup(sessionUrl);
await handleProgress(
id,
RequestProgress.SendingRequest,
() =>
prover.sendRequest(websocketProxyUrl + `?token=${hostname}`, {
url,
method,
headers,
body,
}),
'Error sending request',
);
updateRequestProgress(id, RequestProgress.SendingRequest);
await prover.sendRequest(websocketProxyUrl + `?token=${hostname}`, {
url,
method,
headers,
body,
});
return prover;
return prover;
} catch (error) {
throw error;
}
}
async function verifyProof(proof: PresentationJSON): Promise<{
@@ -545,13 +568,43 @@ async function verifyProof(proof: PresentationJSON): Promise<{
return result;
}
function updateRequestProgress(id: string, progress: RequestProgress) {
devlog(`Request ${id}: ${progressText(progress)}`);
function updateRequestProgress(
id: string,
progress: RequestProgress,
errorMessage?: string,
) {
const progressMessage =
progress === RequestProgress.Error
? `${errorMessage || 'Notarization Failed'}`
: progressText(progress);
devlog(`Request ${id}: ${progressMessage}`);
browser.runtime.sendMessage({
type: BackgroundActiontype.update_request_progress,
data: {
id,
progress: progress,
progress,
errorMessage,
},
});
}
async function handleProgress<T>(
id: string,
progress: RequestProgress,
action: () => Promise<T>,
errorMessage: string,
): Promise<T> {
try {
updateRequestProgress(id, progress);
return await action();
} catch (error: any) {
updateRequestProgress(id, RequestProgress.Error, errorMessage);
await setNotaryRequestStatus(id, 'error');
await setNotaryRequestError(
id,
errorMessage || error.message || 'Unknown error',
);
throw error;
}
}

View File

@@ -13,7 +13,11 @@ import logo from '../../assets/img/icon-128.png';
import classNames from 'classnames';
import Icon from '../../components/Icon';
import { useRequestHistory } from '../../reducers/history';
import { BackgroundActiontype, progressText } from '../Background/rpc';
import {
BackgroundActiontype,
progressText,
RequestProgress,
} from '../Background/rpc';
import { getPluginByHash, getPluginConfigByHash } from '../Background/db';
import { SidePanelActionTypes } from './types';
import { fetchP2PState, useClientId } from '../../reducers/p2p';
@@ -338,17 +342,27 @@ function StepContent(
);
} else if (notaryRequest?.status === 'pending' || pending || notarizationId) {
btnContent = (
<button className="button mt-2 w-fit flex flex-row flex-nowrap items-center gap-2 cursor-default">
<Icon className="animate-spin" fa="fa-solid fa-spinner" size={1} />
<span className="text-sm">
{notaryRequest?.progress
? `(${(
((notaryRequest.progress + 1) / 6.06) *
100
).toFixed()}%) ${progressText(notaryRequest.progress)}`
: 'Pending...'}
</span>
</button>
<div className="flex flex-col gap-2">
{notaryRequest?.progress === RequestProgress.Error && (
<div className="flex flex-row items-center gap-2 text-red-600">
<Icon fa="fa-solid fa-triangle-exclamation" size={1} />
<span className="text-sm">
{notaryRequest?.errorMessage ||
progressText(notaryRequest.progress)}
</span>
</div>
)}
{notaryRequest?.progress !== RequestProgress.Error && (
<button className="button mt-2 w-fit flex flex-row flex-nowrap items-center gap-2 cursor-default">
<Icon className="animate-spin" fa="fa-solid fa-spinner" size={1} />
<span className="text-sm">
{notaryRequest?.progress !== undefined
? `(${(((notaryRequest.progress + 1) / 6.06) * 100).toFixed()}%) ${progressText(notaryRequest.progress)}`
: 'Pending...'}
</span>
</button>
)}
</div>
);
} else {
btnContent = (

View File

@@ -1,23 +1,20 @@
import React, { ReactElement, useState, useCallback } from 'react';
import { useDispatch } from 'react-redux';
import { useNavigate } from 'react-router';
import {
useHistoryOrder,
useRequestHistory,
deleteRequestHistory,
} from '../../reducers/history';
import { useHistoryOrder, useRequestHistory } from '../../reducers/history';
import Icon from '../../components/Icon';
import NotarizeIcon from '../../assets/img/notarize.png';
import { getNotaryApi, getProxyApi } from '../../utils/storage';
import { urlify } from '../../utils/misc';
import {
BackgroundActiontype,
progressText,
RequestProgress,
} from '../../entries/Background/rpc';
import Modal, { ModalContent } from '../../components/Modal/Modal';
import classNames from 'classnames';
import dayjs from 'dayjs';
import RequestMenu from './request-menu';
const charwise = require('charwise');
export default function History(): ReactElement {
@@ -110,12 +107,14 @@ export function OneRequestHistory(props: {
size={1}
/>
<span className="">
{request?.progress
? `(${(
((request.progress + 1) / 6.06) *
100
).toFixed()}%) ${progressText(request.progress)}`
: 'Pending...'}
{request?.progress === RequestProgress.Error
? `${progressText(request.progress, request.errorMessage)}`
: request?.progress
? `(${(
((request.progress + 1) / 6.06) *
100
).toFixed()}%) ${progressText(request.progress)}`
: 'Pending...'}
</span>
</div>
)}
@@ -155,7 +154,7 @@ export function OneRequestHistory(props: {
onClose={closeAllModal}
>
<ModalContent className="flex justify-center items-center text-slate-500">
{msg || 'Something went wrong :('}
{msg || request?.errorMessage}
</ModalContent>
<button
className="m-0 w-24 bg-red-100 text-red-300 hover:bg-red-200 hover:text-red-500"

View File

@@ -1,6 +1,7 @@
import {
BackgroundActiontype,
RequestHistory,
RequestProgress,
} from '../entries/Background/rpc';
import { useSelector } from 'react-redux';
import { AppRootState } from './index';
@@ -76,6 +77,9 @@ export default function history(
if (!payload) return state;
const existing = state.map[payload.id];
if (existing?.progress === RequestProgress.Error) {
return state;
}
const newMap = {
...state.map,
[payload.id]: payload,
@@ -90,13 +94,20 @@ export default function history(
}
case ActionType['/history/setRequests']: {
const payload: RequestHistory[] = action.payload;
const newMap = payload.reduce(
(map: { [id: string]: RequestHistory }, req) => {
if (state.map[req.id]?.progress === RequestProgress.Error) {
map[req.id] = state.map[req.id];
} else {
map[req.id] = req;
}
return map;
},
{},
);
return {
...state,
map: payload.reduce((map: { [id: string]: RequestHistory }, req) => {
map[req.id] = req;
return map;
}, {}),
map: newMap,
order: payload.map(({ id }) => id),
};
}