- Notifications
You must be signed in to change notification settings - Fork 50
feat: subgraph support for shutter disputekit in devnet #1966
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
317aed6 05dcdb0 44ea55e a995e1e f3f235c 78b2951 File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| | @@ -9,7 +9,7 @@ import { | |||||||||||||||||||||||||||||||||||||||
| CommitCast, | ||||||||||||||||||||||||||||||||||||||||
| } from "../generated/DisputeKitClassic/DisputeKitClassic"; | ||||||||||||||||||||||||||||||||||||||||
| import { KlerosCore } from "../generated/KlerosCore/KlerosCore"; | ||||||||||||||||||||||||||||||||||||||||
| import { ClassicDispute, ClassicJustification, ClassicRound, ClassicVote, Dispute } from "../generated/schema"; | ||||||||||||||||||||||||||||||||||||||||
| import { ClassicDispute, ClassicJustification, ClassicRound, ClassicVote, Dispute, Round } from "../generated/schema"; | ||||||||||||||||||||||||||||||||||||||||
| import { ensureClassicContributionFromEvent } from "./entities/ClassicContribution"; | ||||||||||||||||||||||||||||||||||||||||
| import { createClassicDisputeFromEvent } from "./entities/ClassicDispute"; | ||||||||||||||||||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||||||||||||||||||
| | @@ -19,23 +19,36 @@ import { | |||||||||||||||||||||||||||||||||||||||
| updateCountsAndGetCurrentRuling, | ||||||||||||||||||||||||||||||||||||||||
| } from "./entities/ClassicRound"; | ||||||||||||||||||||||||||||||||||||||||
| import { ensureClassicVote } from "./entities/ClassicVote"; | ||||||||||||||||||||||||||||||||||||||||
| import { ONE, ZERO } from "./utils"; | ||||||||||||||||||||||||||||||||||||||||
| | ||||||||||||||||||||||||||||||||||||||||
| export const DISPUTEKIT_ID = "1"; | ||||||||||||||||||||||||||||||||||||||||
| import { ONE, extractDisputeKitIDFromExtraData } from "./utils"; | ||||||||||||||||||||||||||||||||||||||||
| | ||||||||||||||||||||||||||||||||||||||||
| export function handleDisputeCreation(event: DisputeCreation): void { | ||||||||||||||||||||||||||||||||||||||||
| const disputeID = event.params._coreDisputeID.toString(); | ||||||||||||||||||||||||||||||||||||||||
| createClassicDisputeFromEvent(event); | ||||||||||||||||||||||||||||||||||||||||
| const numberOfChoices = event.params._numberOfChoices; | ||||||||||||||||||||||||||||||||||||||||
| createClassicRound(disputeID, numberOfChoices, ZERO); | ||||||||||||||||||||||||||||||||||||||||
| const disputeKitID = extractDisputeKitIDFromExtraData(event.params._extraData); | ||||||||||||||||||||||||||||||||||||||||
| | ||||||||||||||||||||||||||||||||||||||||
| const disputeKitClassic = DisputeKitClassic.bind(event.address); | ||||||||||||||||||||||||||||||||||||||||
| const klerosCore = KlerosCore.bind(disputeKitClassic.core()); | ||||||||||||||||||||||||||||||||||||||||
| const totalRounds = klerosCore.getNumberOfRounds(event.params._coreDisputeID); | ||||||||||||||||||||||||||||||||||||||||
| const newRoundIndex = totalRounds.minus(ONE); | ||||||||||||||||||||||||||||||||||||||||
| | ||||||||||||||||||||||||||||||||||||||||
| createClassicDisputeFromEvent(event, disputeKitID, newRoundIndex); | ||||||||||||||||||||||||||||||||||||||||
| createClassicRound(disputeID, event.params._numberOfChoices, newRoundIndex, disputeKitID); | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| | ||||||||||||||||||||||||||||||||||||||||
| export function handleCommitCast(event: CommitCast): void { | ||||||||||||||||||||||||||||||||||||||||
| const coreDisputeID = event.params._coreDisputeID; | ||||||||||||||||||||||||||||||||||||||||
| const coreDispute = Dispute.load(coreDisputeID.toString()); | ||||||||||||||||||||||||||||||||||||||||
| const classicDisputeID = `${DISPUTEKIT_ID}-${coreDisputeID}`; | ||||||||||||||||||||||||||||||||||||||||
| const coreDisputeID = event.params._coreDisputeID.toString(); | ||||||||||||||||||||||||||||||||||||||||
| const coreDispute = Dispute.load(coreDisputeID); | ||||||||||||||||||||||||||||||||||||||||
| if (!coreDispute) return; | ||||||||||||||||||||||||||||||||||||||||
| | ||||||||||||||||||||||||||||||||||||||||
| const coreCurrentRound = Round.load(coreDispute.currentRound); | ||||||||||||||||||||||||||||||||||||||||
| if (!coreCurrentRound) return; | ||||||||||||||||||||||||||||||||||||||||
| | ||||||||||||||||||||||||||||||||||||||||
| const disputeKitID = coreCurrentRound.disputeKit; | ||||||||||||||||||||||||||||||||||||||||
| | ||||||||||||||||||||||||||||||||||||||||
| const classicDisputeID = `${disputeKitID}-${coreDisputeID}`; | ||||||||||||||||||||||||||||||||||||||||
| | ||||||||||||||||||||||||||||||||||||||||
| const classicDispute = ClassicDispute.load(classicDisputeID); | ||||||||||||||||||||||||||||||||||||||||
| if (!classicDispute || !coreDispute) return; | ||||||||||||||||||||||||||||||||||||||||
| if (!classicDispute) return; | ||||||||||||||||||||||||||||||||||||||||
| | ||||||||||||||||||||||||||||||||||||||||
| const currentLocalRoundID = classicDispute.id + "-" + classicDispute.currentLocalRoundIndex.toString(); | ||||||||||||||||||||||||||||||||||||||||
| const voteIDs = event.params._voteIDs; | ||||||||||||||||||||||||||||||||||||||||
| for (let i = 0; i < voteIDs.length; i++) { | ||||||||||||||||||||||||||||||||||||||||
| | @@ -55,9 +68,18 @@ export function handleVoteCast(event: VoteCast): void { | |||||||||||||||||||||||||||||||||||||||
| const juror = event.params._juror.toHexString(); | ||||||||||||||||||||||||||||||||||||||||
| const coreDisputeID = event.params._coreDisputeID.toString(); | ||||||||||||||||||||||||||||||||||||||||
| const coreDispute = Dispute.load(coreDisputeID); | ||||||||||||||||||||||||||||||||||||||||
| const classicDisputeID = `${DISPUTEKIT_ID}-${coreDisputeID}`; | ||||||||||||||||||||||||||||||||||||||||
| if (!coreDispute) return; | ||||||||||||||||||||||||||||||||||||||||
| | ||||||||||||||||||||||||||||||||||||||||
| const coreCurrentRound = Round.load(coreDispute.currentRound); | ||||||||||||||||||||||||||||||||||||||||
| if (!coreCurrentRound) return; | ||||||||||||||||||||||||||||||||||||||||
| | ||||||||||||||||||||||||||||||||||||||||
| const disputeKitID = coreCurrentRound.disputeKit; | ||||||||||||||||||||||||||||||||||||||||
| | ||||||||||||||||||||||||||||||||||||||||
| const classicDisputeID = `${disputeKitID}-${coreDisputeID}`; | ||||||||||||||||||||||||||||||||||||||||
| | ||||||||||||||||||||||||||||||||||||||||
| const classicDispute = ClassicDispute.load(classicDisputeID); | ||||||||||||||||||||||||||||||||||||||||
| if (!classicDispute || !coreDispute) return; | ||||||||||||||||||||||||||||||||||||||||
| if (!classicDispute) return; | ||||||||||||||||||||||||||||||||||||||||
| | ||||||||||||||||||||||||||||||||||||||||
| const choice = event.params._choice; | ||||||||||||||||||||||||||||||||||||||||
| const currentLocalRoundID = classicDispute.id + "-" + classicDispute.currentLocalRoundIndex.toString(); | ||||||||||||||||||||||||||||||||||||||||
| const voteIDs = event.params._voteIDs; | ||||||||||||||||||||||||||||||||||||||||
| | @@ -70,6 +92,7 @@ export function handleVoteCast(event: VoteCast): void { | |||||||||||||||||||||||||||||||||||||||
| justification.transactionHash = event.transaction.hash.toHexString(); | ||||||||||||||||||||||||||||||||||||||||
| justification.timestamp = event.block.timestamp; | ||||||||||||||||||||||||||||||||||||||||
| justification.save(); | ||||||||||||||||||||||||||||||||||||||||
| | ||||||||||||||||||||||||||||||||||||||||
| const currentRulingInfo = updateCountsAndGetCurrentRuling( | ||||||||||||||||||||||||||||||||||||||||
| currentLocalRoundID, | ||||||||||||||||||||||||||||||||||||||||
| choice, | ||||||||||||||||||||||||||||||||||||||||
| | @@ -78,6 +101,7 @@ export function handleVoteCast(event: VoteCast): void { | |||||||||||||||||||||||||||||||||||||||
| coreDispute.currentRuling = currentRulingInfo.ruling; | ||||||||||||||||||||||||||||||||||||||||
| coreDispute.tied = currentRulingInfo.tied; | ||||||||||||||||||||||||||||||||||||||||
| coreDispute.save(); | ||||||||||||||||||||||||||||||||||||||||
| | ||||||||||||||||||||||||||||||||||||||||
| let classicVote: ClassicVote; | ||||||||||||||||||||||||||||||||||||||||
| for (let i = 0; i < voteIDs.length; i++) { | ||||||||||||||||||||||||||||||||||||||||
| classicVote = ensureClassicVote(currentLocalRoundID, juror, voteIDs[i], coreDispute); | ||||||||||||||||||||||||||||||||||||||||
| | @@ -97,7 +121,16 @@ export function handleChoiceFunded(event: ChoiceFunded): void { | |||||||||||||||||||||||||||||||||||||||
| const coreDisputeID = event.params._coreDisputeID.toString(); | ||||||||||||||||||||||||||||||||||||||||
| const coreRoundIndex = event.params._coreRoundID.toString(); | ||||||||||||||||||||||||||||||||||||||||
| const choice = event.params._choice; | ||||||||||||||||||||||||||||||||||||||||
| const roundID = `${DISPUTEKIT_ID}-${coreDisputeID}-${coreRoundIndex}`; | ||||||||||||||||||||||||||||||||||||||||
| | ||||||||||||||||||||||||||||||||||||||||
| const coreDispute = Dispute.load(coreDisputeID); | ||||||||||||||||||||||||||||||||||||||||
| if (!coreDispute) return; | ||||||||||||||||||||||||||||||||||||||||
| | ||||||||||||||||||||||||||||||||||||||||
| const roundId = `${coreDisputeID}-${coreRoundIndex}`; | ||||||||||||||||||||||||||||||||||||||||
| const coreRound = Round.load(roundId); | ||||||||||||||||||||||||||||||||||||||||
| if (!coreRound) return; | ||||||||||||||||||||||||||||||||||||||||
| const disputeKitID = coreRound.disputeKit; | ||||||||||||||||||||||||||||||||||||||||
| | ||||||||||||||||||||||||||||||||||||||||
| const roundID = `${disputeKitID}-${coreDisputeID}-${coreRoundIndex}`; | ||||||||||||||||||||||||||||||||||||||||
| Comment on lines +125 to +133 Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Early exit when Same defensive check as earlier – without it, const disputeKitID = coreRound.disputeKit; - + if (!disputeKitID) return;📝 Committable suggestion
Suggested change
| ||||||||||||||||||||||||||||||||||||||||
| | ||||||||||||||||||||||||||||||||||||||||
| const localRound = ClassicRound.load(roundID); | ||||||||||||||||||||||||||||||||||||||||
| if (!localRound) return; | ||||||||||||||||||||||||||||||||||||||||
| | @@ -120,16 +153,23 @@ export function handleChoiceFunded(event: ChoiceFunded): void { | |||||||||||||||||||||||||||||||||||||||
| const numberOfRounds = klerosCore.getNumberOfRounds(BigInt.fromString(coreDisputeID)); | ||||||||||||||||||||||||||||||||||||||||
| const roundInfo = klerosCore.getRoundInfo(BigInt.fromString(coreDisputeID), numberOfRounds.minus(ONE)); | ||||||||||||||||||||||||||||||||||||||||
| const appealCost = roundInfo.totalFeesForJurors; | ||||||||||||||||||||||||||||||||||||||||
| const currentDisputeKitID = roundInfo.disputeKitID; | ||||||||||||||||||||||||||||||||||||||||
| | ||||||||||||||||||||||||||||||||||||||||
| localRound.feeRewards = localRound.feeRewards.minus(appealCost); | ||||||||||||||||||||||||||||||||||||||||
| | ||||||||||||||||||||||||||||||||||||||||
| const localDispute = ClassicDispute.load(`${DISPUTEKIT_ID}-${coreDisputeID}`); | ||||||||||||||||||||||||||||||||||||||||
| const newRoundInfo = klerosCore.getRoundInfo(BigInt.fromString(coreDisputeID), numberOfRounds); | ||||||||||||||||||||||||||||||||||||||||
| const newDisputeKitID = newRoundInfo.disputeKitID; | ||||||||||||||||||||||||||||||||||||||||
| | ||||||||||||||||||||||||||||||||||||||||
| const localDispute = ClassicDispute.load(`${disputeKitID}-${coreDisputeID}`); | ||||||||||||||||||||||||||||||||||||||||
| if (!localDispute) return; | ||||||||||||||||||||||||||||||||||||||||
| const newRoundIndex = localDispute.currentLocalRoundIndex.plus(ONE); | ||||||||||||||||||||||||||||||||||||||||
| const numberOfChoices = localDispute.numberOfChoices; | ||||||||||||||||||||||||||||||||||||||||
| localDispute.currentLocalRoundIndex = newRoundIndex; | ||||||||||||||||||||||||||||||||||||||||
| localDispute.save(); | ||||||||||||||||||||||||||||||||||||||||
| createClassicRound(coreDisputeID, numberOfChoices, newRoundIndex); | ||||||||||||||||||||||||||||||||||||||||
| | ||||||||||||||||||||||||||||||||||||||||
| if (currentDisputeKitID === newDisputeKitID) { | ||||||||||||||||||||||||||||||||||||||||
| const newRoundIndex = localDispute.currentLocalRoundIndex.plus(ONE); | ||||||||||||||||||||||||||||||||||||||||
| const numberOfChoices = localDispute.numberOfChoices; | ||||||||||||||||||||||||||||||||||||||||
| localDispute.currentLocalRoundIndex = newRoundIndex; | ||||||||||||||||||||||||||||||||||||||||
| localDispute.save(); | ||||||||||||||||||||||||||||||||||||||||
| createClassicRound(coreDisputeID, numberOfChoices, newRoundIndex, disputeKitID); | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| | ||||||||||||||||||||||||||||||||||||||||
| localRound.save(); | ||||||||||||||||||||||||||||||||||||||||
| | @@ -144,7 +184,16 @@ export function handleWithdrawal(event: Withdrawal): void { | |||||||||||||||||||||||||||||||||||||||
| // check if all appeal fees have been withdrawn | ||||||||||||||||||||||||||||||||||||||||
| const coreDisputeID = event.params._coreDisputeID.toString(); | ||||||||||||||||||||||||||||||||||||||||
| const coreRoundIndex = event.params._coreRoundID.toString(); | ||||||||||||||||||||||||||||||||||||||||
| const roundID = `${DISPUTEKIT_ID}-${coreDisputeID}-${coreRoundIndex}`; | ||||||||||||||||||||||||||||||||||||||||
| | ||||||||||||||||||||||||||||||||||||||||
| const coreDispute = Dispute.load(coreDisputeID); | ||||||||||||||||||||||||||||||||||||||||
| if (!coreDispute) return; | ||||||||||||||||||||||||||||||||||||||||
| | ||||||||||||||||||||||||||||||||||||||||
| const roundId = `${coreDisputeID}-${coreRoundIndex}`; | ||||||||||||||||||||||||||||||||||||||||
| const coreRound = Round.load(roundId); | ||||||||||||||||||||||||||||||||||||||||
| if (!coreRound) return; | ||||||||||||||||||||||||||||||||||||||||
| const disputeKitID = coreRound.disputeKit; | ||||||||||||||||||||||||||||||||||||||||
| | ||||||||||||||||||||||||||||||||||||||||
| const roundID = `${disputeKitID}-${coreDisputeID}-${coreRoundIndex}`; | ||||||||||||||||||||||||||||||||||||||||
| Comment on lines +186 to +194 Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Defensive check for Replicating the guard here avoids malformed IDs during withdrawals. const disputeKitID = coreRound.disputeKit; - + if (!disputeKitID) return;📝 Committable suggestion
Suggested change
| ||||||||||||||||||||||||||||||||||||||||
| | ||||||||||||||||||||||||||||||||||||||||
| Comment on lines +186 to 195 Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Missing defensive check in
| ||||||||||||||||||||||||||||||||||||||||
| const localRound = ClassicRound.load(roundID); | ||||||||||||||||||||||||||||||||||||||||
| if (!localRound) return; | ||||||||||||||||||||||||||||||||||||||||
| | ||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,13 +1,23 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import { ClassicContribution } from "../../generated/schema"; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import { ClassicContribution, Dispute, DisputeKit, Round } from "../../generated/schema"; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import { Contribution as ContributionEvent, Withdrawal } from "../../generated/DisputeKitClassic/DisputeKitClassic"; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import { DISPUTEKIT_ID } from "../DisputeKitClassic"; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import { ensureUser } from "./User"; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| | ||||||||||||||||||||||||||||||||||||||||||||||||||
| export function ensureClassicContributionFromEvent<T>(event: T): ClassicContribution | null { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!(event instanceof ContributionEvent) && !(event instanceof Withdrawal)) return null; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const coreDisputeID = event.params._coreDisputeID.toString(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const coreRoundIndex = event.params._coreRoundID.toString(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const roundID = `${DISPUTEKIT_ID}-${coreDisputeID}-${coreRoundIndex}`; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const coreDispute = Dispute.load(coreDisputeID); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!coreDispute) return null; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const roundId = `${coreDisputeID}-${coreRoundIndex}`; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const coreRound = Round.load(roundId); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!coreRound) return null; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const disputeKitID = coreRound.disputeKit; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const roundID = `${disputeKitID}-${coreDisputeID}-${coreRoundIndex}`; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| | ||||||||||||||||||||||||||||||||||||||||||||||||||
| Comment on lines +10 to +20 Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chainGuard against a missing
- const disputeKitID = coreRound.disputeKit; + const disputeKitID = coreRound.disputeKit; + if (!disputeKitID) { + // Inconsistent data – abort processing to avoid polluting the store + return null; + }Fail-fast avoids persisting wrongly-keyed entities and makes the issue visible during indexing. 🏁 Script executed: #!/bin/bash echo "=== GraphQL schema files ===" find . -type f -name "*.graphql" echo "" echo "=== 'type Round' and 'disputeKit' in GraphQL schemas ===" rg -n "type Round" -C2 -g "*.graphql" || true rg -n "disputeKit" -C2 -g "*.graphql" || true echo "" echo "=== Locate and inspect Round entity class ===" round_files=$(rg -l "export class Round" -g "*.ts") echo "Found Round.ts files:" echo "$round_files" for file in $round_files; do echo "" echo "------ $file ------" rg -n "export class Round" -C3 "$file" rg -n "get disputeKit" -C5 "$file" || true doneLength of output: 3678 Guard against a missing The GraphQL schema defines • File: - const disputeKitID = coreRound.disputeKit; + const disputeKitID = coreRound.disputeKit; + if (!disputeKitID) { + // Inconsistent data – abort processing to avoid polluting the store + return null; + }This fail-fast guard ensures any missing 📝 Committable suggestion
Suggested change
| ||||||||||||||||||||||||||||||||||||||||||||||||||
| ensureUser(event.params._contributor.toHexString()); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const contributor = event.params._contributor.toHexString(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const choice = event.params._choice; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| | ||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,10 @@ | ||
| import { BigInt } from "@graphprotocol/graph-ts"; | ||
| import { BigInt, Bytes } from "@graphprotocol/graph-ts"; | ||
| | ||
| export const ZERO = BigInt.fromI32(0); | ||
| export const ONE = BigInt.fromI32(1); | ||
| | ||
| export function extractDisputeKitIDFromExtraData(extraData: Bytes): string { | ||
| const start = extraData.length - 32; | ||
| const littleEndian = extraData.subarray(start, extraData.length).reverse(); | ||
| return BigInt.fromUnsignedBytes(Bytes.fromUint8Array(littleEndian)).toString(); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Add a null / empty check for
disputeKitIDbefore composing entity IDsextractDisputeKitIDFromExtraData()can legitimately fail (e.g. malformed or zero-length extraData) and return an empty string.Using that value immediately creates IDs such as
"null-123"which later break.load()look-ups and triggers hard-to-trace “entity not found” errors.This defensive guard is especially important now that the same code path indexes multiple kits.
📝 Committable suggestion