Skip to content

Commit 7744265

Browse files
authored
Harden claim observation and complete app audit (#3)
1 parent d2e46de commit 7744265

23 files changed

Lines changed: 682 additions & 188 deletions

‎README.md‎

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ A source-changing hardening release is deployed and certified on Studio Next:
132132
`dfb564fbd644fae756808fee2afc1f43c35d0dde095e33ad9f4802569e80007a`
133133
- Direct Runtime: **117 passed**
134134
- Python non-runtime: **454 passed + 334 subtests**
135-
- frontend Vitest: **43 passed**
135+
- frontend Vitest: **46 passed**
136136
- browser E2E: **12 passed**
137137

138138
The candidate rejects future-dated evidence, exposes the exact helper binding,
@@ -213,12 +213,15 @@ ABORT branches and one-time entitlement consumption. It does not claim
213213
authenticated proof of downstream external delivery or production-grade
214214
retry/reconciliation for failed external transfers.
215215

216-
The frontend now provides a fail-closed operator observation of triggered child
217-
transactions. It reports `DELIVERED` only for an exact one-child match with
218-
`FINALIZED` plus `FINISHED_WITH_RETURN`, reports `FAILED` only for an exact
219-
match with `FINISHED_WITH_ERROR`, and otherwise reports `PENDING` or
220-
`UNVERIFIED`. This read-only observation does not change the contract boundary
221-
or authorize a retry.
216+
The frontend now provides a fail-closed operator observation of claim parents,
217+
their exact native outbound message, and any triggered child transaction. It
218+
reports `DELIVERED` only after the parent is independently bound to the
219+
certified coordinator, exact `claim_mission` call, connected beneficiary, and
220+
one exact outbound message, followed by an exact child match with `FINALIZED`
221+
plus `FINISHED_WITH_RETURN`. A finalized child execution error is reported as
222+
`FINALIZED_ERROR`, not proof of terminal non-delivery. Missing, ambiguous, or
223+
unreadable child data remains `UNVERIFIED`. This read-only observation does not
224+
change the contract boundary or authorize a retry.
222225

223226
See [`docs/OPEN_QUESTIONS.md`](./docs/OPEN_QUESTIONS.md) for the concise submission boundary.
224227

‎components/application/application-shell.tsx‎

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -353,7 +353,7 @@ export function ApplicationShell() {
353353
</div>
354354
) : (
355355
<CaseRoom
356-
key={lastMissionId || "case-room"}
356+
key={`${lastMissionId || "case-room"}:${wallet.address}`}
357357
wallet={wallet}
358358
initialMissionId={lastMissionId}
359359
/>
@@ -420,7 +420,7 @@ export function ApplicationShell() {
420420
</div>
421421
) : (
422422
<FundMissionFlow
423-
key={lastMissionId || "manual-funding"}
423+
key={`${lastMissionId || "manual-funding"}:${wallet.address}`}
424424
wallet={wallet}
425425
initialMissionId={lastMissionId}
426426
/>
@@ -443,7 +443,7 @@ export function ApplicationShell() {
443443
</div>
444444
) : (
445445
<PrepareMissionFlow
446-
key={lastMissionId || "manual-prepare"}
446+
key={`${lastMissionId || "manual-prepare"}:${wallet.address}`}
447447
wallet={wallet}
448448
initialMissionId={lastMissionId}
449449
/>
@@ -466,7 +466,7 @@ export function ApplicationShell() {
466466
</div>
467467
) : (
468468
<EvidenceMissionFlow
469-
key={lastMissionId || "manual-evidence"}
469+
key={`${lastMissionId || "manual-evidence"}:${wallet.address}`}
470470
wallet={wallet}
471471
initialMissionId={lastMissionId}
472472
/>
@@ -489,7 +489,7 @@ export function ApplicationShell() {
489489
</div>
490490
) : (
491491
<SealMissionFlow
492-
key={lastMissionId || "manual-seal"}
492+
key={`${lastMissionId || "manual-seal"}:${wallet.address}`}
493493
wallet={wallet}
494494
initialMissionId={lastMissionId}
495495
/>
@@ -512,7 +512,7 @@ export function ApplicationShell() {
512512
</div>
513513
) : (
514514
<ResolutionMissionFlow
515-
key={lastMissionId || "manual-resolution"}
515+
key={`${lastMissionId || "manual-resolution"}:${wallet.address}`}
516516
wallet={wallet}
517517
initialMissionId={lastMissionId}
518518
/>
@@ -535,7 +535,7 @@ export function ApplicationShell() {
535535
</div>
536536
) : (
537537
<ClaimMissionFlow
538-
key={lastMissionId || "manual-claim"}
538+
key={`${lastMissionId || "manual-claim"}:${wallet.address}`}
539539
wallet={wallet}
540540
initialMissionId={lastMissionId}
541541
/>

‎components/application/claim-mission-flow.tsx‎

Lines changed: 59 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ import {
2424
type FinalizedClaimProof,
2525
} from "@/lib/genlayer-claim";
2626
import {
27+
CURRENT_DEPLOYMENT_ANCHOR,
28+
} from "@/lib/deployment-anchor";
29+
import {
30+
isTransactionHash,
2731
observeExternalDelivery,
2832
persistClaimTransaction,
2933
readPersistedClaimTransaction,
@@ -84,6 +88,12 @@ export function ClaimMissionFlow({
8488
wallet.address,
8589
),
8690
);
91+
const [claimTransactionId, setClaimTransactionId] = useState(() =>
92+
readPersistedClaimTransaction(
93+
initialMissionId,
94+
wallet.address,
95+
)?.transactionId ?? "",
96+
);
8797
const [
8898
delivery,
8999
setDelivery,
@@ -101,6 +111,11 @@ export function ClaimMissionFlow({
101111
&& claimRecord.beneficiary.toLowerCase() === wallet.address.toLowerCase()
102112
? claimRecord
103113
: null;
114+
const activeParentClaimTransactionId = isTransactionHash(
115+
claimTransactionId.trim(),
116+
)
117+
? claimTransactionId.trim()
118+
: null;
104119

105120
function resetReview() {
106121
setQuote(null);
@@ -162,6 +177,7 @@ export function ClaimMissionFlow({
162177
transactionId: txId,
163178
amount: quote.snapshot.missionClaimable,
164179
});
180+
setClaimTransactionId(txId);
165181
persistClaimTransaction(
166182
quote.snapshot.mission.missionId,
167183
quote.snapshot.beneficiary,
@@ -181,6 +197,8 @@ export function ClaimMissionFlow({
181197
wallet.client,
182198
txId,
183199
{
200+
coordinator: CURRENT_DEPLOYMENT_ANCHOR.contractAddress,
201+
missionId: quote.snapshot.mission.missionId,
184202
recipient: quote.snapshot.beneficiary,
185203
amount: quote.snapshot.missionClaimable,
186204
},
@@ -215,10 +233,7 @@ export function ClaimMissionFlow({
215233
}
216234

217235
async function refreshDelivery() {
218-
if (
219-
activeClaimRecord === null
220-
|| deliveryBusy
221-
) {
236+
if (activeParentClaimTransactionId === null || deliveryBusy) {
222237
return;
223238
}
224239

@@ -228,17 +243,19 @@ export function ClaimMissionFlow({
228243
setDelivery(
229244
await observeExternalDelivery(
230245
wallet.client,
231-
activeClaimRecord.transactionId,
246+
activeParentClaimTransactionId,
232247
{
248+
coordinator: CURRENT_DEPLOYMENT_ANCHOR.contractAddress,
249+
missionId,
233250
recipient: wallet.address,
234-
amount: activeClaimRecord.amount,
251+
amount: activeClaimRecord?.amount,
235252
},
236253
),
237254
);
238255
} catch (caught: unknown) {
239256
setDelivery(
240257
unverifiedExternalDelivery(
241-
activeClaimRecord.transactionId,
258+
activeParentClaimTransactionId,
242259
caught instanceof Error
243260
? caught.message
244261
: "The delivery observation could not be completed. No delivery outcome is claimed.",
@@ -254,8 +271,8 @@ export function ClaimMissionFlow({
254271
): string {
255272
return phase === "DELIVERED"
256273
? "DELIVERED"
257-
: phase === "FAILED"
258-
? "FAILED — NO AUTOMATIC RETRY"
274+
: phase === "FINALIZED_ERROR"
275+
? "FINALIZED EXECUTION ERROR — DELIVERY UNRESOLVED"
259276
: phase === "PENDING"
260277
? "PENDING"
261278
: "UNVERIFIED";
@@ -285,19 +302,33 @@ export function ClaimMissionFlow({
285302
setMissionId(
286303
nextMissionId,
287304
);
288-
setClaimRecord(
289-
readPersistedClaimTransaction(
290-
nextMissionId,
291-
wallet.address,
292-
),
305+
const persisted = readPersistedClaimTransaction(
306+
nextMissionId,
307+
wallet.address,
293308
);
309+
setClaimRecord(persisted);
310+
setClaimTransactionId(persisted?.transactionId ?? "");
294311
resetReview();
295312
}}
296313
placeholder="Mission ID"
297314
maxLength={512}
298315
/>
299316
</label>
300317

318+
<label className="claim-mission-input">
319+
<span>Parent claim transaction (optional recovery)</span>
320+
<input
321+
value={claimTransactionId}
322+
onChange={(event) => {
323+
setClaimTransactionId(event.target.value);
324+
setDelivery(null);
325+
setError(null);
326+
}}
327+
placeholder="0x… paste a finalized claim transaction"
328+
spellCheck={false}
329+
/>
330+
</label>
331+
301332
<div className="claim-rule-grid">
302333
<div>
303334
<ShieldCheck
@@ -522,7 +553,7 @@ export function ClaimMissionFlow({
522553
</div>
523554
) : null}
524555

525-
{activeClaimRecord !== null ? (
556+
{activeParentClaimTransactionId !== null ? (
526557
<div className="claim-delivery">
527558
<div className="claim-delivery-heading">
528559
<div>
@@ -559,7 +590,16 @@ export function ClaimMissionFlow({
559590
<dl>
560591
<div>
561592
<dt>Parent claim</dt>
562-
<dd>{activeClaimRecord.transactionId}</dd>
593+
<dd>{activeParentClaimTransactionId}</dd>
594+
</div>
595+
<div>
596+
<dt>Outbound message</dt>
597+
<dd>
598+
{delivery?.outboundMessage === null
599+
|| delivery?.outboundMessage === undefined
600+
? "Not verified"
601+
: `${delivery.outboundMessage.recipient} · ${delivery.outboundMessage.amount.toString()} wei`}
602+
</dd>
563603
</div>
564604
<div>
565605
<dt>Triggered child</dt>
@@ -587,8 +627,9 @@ export function ClaimMissionFlow({
587627
<small>{delivery.reason}</small>
588628
) : (
589629
<small>
590-
Select “Observe child transaction” to read the exact child
591-
transaction. No delivery outcome is assumed before then.
630+
Select “Observe child transaction” to verify the parent claim,
631+
outbound message, and any exact child transaction exposed by
632+
Studio Next. No delivery outcome is assumed before then.
592633
</small>
593634
)}
594635
</div>

‎components/application/create-mission-flow.tsx‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
type MissionDraft,
2525
type TransactionProgress,
2626
} from "@/lib/genlayer-browser";
27+
import { writeBrowserStorage } from "@/lib/browser-storage";
2728

2829
type CreateMissionFlowProps = {
2930
wallet: ConnectedCommitWallet;
@@ -256,7 +257,7 @@ export function CreateMissionFlow({
256257
quote,
257258
);
258259

259-
localStorage.setItem(
260+
writeBrowserStorage(
260261
"commit:last-transaction",
261262
txId,
262263
);
@@ -273,7 +274,7 @@ export function CreateMissionFlow({
273274
);
274275

275276
if (finalResult.successful) {
276-
localStorage.setItem(
277+
writeBrowserStorage(
277278
"commit:last-mission",
278279
missionId,
279280
);

‎components/application/fund-mission-flow.tsx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
type MissionFundingSnapshot,
2222
type TransactionProgress,
2323
} from "@/lib/genlayer-browser";
24+
import { writeBrowserStorage } from "@/lib/browser-storage";
2425

2526
type FundMissionFlowProps = {
2627
wallet: ConnectedCommitWallet;
@@ -169,7 +170,7 @@ export function FundMissionFlow({
169170
quote,
170171
);
171172

172-
localStorage.setItem(
173+
writeBrowserStorage(
173174
"commit:last-transaction",
174175
txId,
175176
);

‎components/application/resolution-mission-flow.tsx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import {
3535
type RepairEvidenceQuote,
3636
type ResolutionInspection,
3737
} from "@/lib/genlayer-resolution";
38+
import { writeBrowserStorage } from "@/lib/browser-storage";
3839

3940
type ResolutionMissionFlowProps = {
4041
wallet: ConnectedCommitWallet;
@@ -249,7 +250,7 @@ export function ResolutionMissionFlow({
249250
evaluateQuote,
250251
);
251252

252-
localStorage.setItem(
253+
writeBrowserStorage(
253254
`commit:evaluation:${missionId}`,
254255
txId,
255256
);

‎components/justice/case-room.tsx‎

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import {
3535
} from "@/lib/genlayer-appeal";
3636
import { AppealPanel } from "@/components/justice/appeal-panel";
3737
import { EvidenceRecordCard } from "@/components/justice/evidence-record-card";
38+
import { readBrowserStorage } from "@/lib/browser-storage";
3839

3940
type CaseRoomProps = {
4041
wallet: ConnectedCommitWallet;
@@ -156,7 +157,7 @@ export function CaseRoom({
156157
return "";
157158
}
158159

159-
return window.localStorage.getItem(
160+
return readBrowserStorage(
160161
`commit:evaluation:${initialMissionId}`,
161162
) ?? "";
162163
});
@@ -247,10 +248,10 @@ export function CaseRoom({
247248
);
248249

249250
setSubmittedAppeal(txId);
250-
setAppealBusy(false);
251-
void refreshCase();
251+
await refreshCase();
252252
} catch (caught: unknown) {
253253
setAppealError(errorMessage(caught));
254+
} finally {
254255
setAppealBusy(false);
255256
}
256257
}
@@ -274,11 +275,9 @@ export function CaseRoom({
274275
const nextMissionId = event.target.value;
275276
setMissionId(nextMissionId);
276277
setEvaluationTxId(
277-
typeof window === "undefined"
278-
? ""
279-
: window.localStorage.getItem(
280-
`commit:evaluation:${nextMissionId}`,
281-
) ?? "",
278+
readBrowserStorage(
279+
`commit:evaluation:${nextMissionId}`,
280+
) ?? "",
282281
);
283282
setCaseData(null);
284283
setError(null);

‎docs/AGENT_TANK_SUBMISSION.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ The earlier published release has the following historical certification record:
138138
- helper source SHA-256:
139139
`dfb564fbd644fae756808fee2afc1f43c35d0dde095e33ad9f4802569e80007a`;
140140
- Python regression: **450 passed + 334 subtests**;
141-
- frontend Vitest: **27 passed**;
141+
- frontend Vitest: **46 passed**;
142142
- browser E2E: **12 passed**;
143143
- public `/`, `/app`, `/verify`, `/api/v1/health`: **HTTP 200**;
144144
- public production bodies matched the promoted production clone during final

‎docs/CURRENT_DEPLOYMENT_PROOF_2026-09-16.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@ Local F18 evidence packet index SHA-256:
6161

6262
## Public application release
6363

64-
- Public URL: https://commitprotocol-genlayer.vercel.app
64+
- Historical public URL: https://commitprotocol-genlayer.vercel.app
65+
- Current canonical public URL: https://commit-protocol.vercel.app
6566
- GitHub main: `e9858985495111cf2f21db6dc847c7f75b79c0da`
6667
- Vercel production deployment ID: `dpl_6x9XzgAxPsRX8jXr7d6KonmEmL6S`
6768
- Production release tree: `3627b57a0baaebda07b15da76cb8b594ee4a18f9`

0 commit comments

Comments
 (0)