Skip to content

Commit 7459dca

Browse files
Merge pull request #201 from IntersectMBO/fix/cert-redeemer-evaluation
fix(evaluation): normalize Blockfrost JSONWSP v5 redeemer tags and fail fast on unmatched results
2 parents b5b75aa + 619c52b commit 7459dca

3 files changed

Lines changed: 75 additions & 26 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@evolution-sdk/evolution": patch
3+
---
4+
5+
Script transactions with certificate or withdrawal redeemers evaluated via Blockfrost no longer spam warning logs or loop indefinitely. Blockfrost's Ogmios v5 JSONWSP format returns `"certificate:N"` and `"withdrawal:N"` as redeemer pointer keys; these are now normalized to the canonical `"cert"` and `"reward"` tags before evaluation matching. Unmatched redeemer tags from any evaluator now fail immediately instead of silently leaving ExUnits at zero.

packages/evolution/src/sdk/builders/phases/Evaluation.ts

Lines changed: 65 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -162,8 +162,11 @@ const resolveDeferredRedeemers = (
162162
const utxo = refToUtxo.get(key)
163163

164164
if (index === undefined || !utxo) {
165-
yield* Effect.logWarning(`[Evaluation] Could not resolve self redeemer for ${key} - UTxO not in transaction`)
166-
continue
165+
return yield* Effect.fail(
166+
new TransactionBuilderError({
167+
message: `Self redeemer for ${key} could not be resolved: UTxO is not present in the transaction inputs`
168+
})
169+
)
167170
}
168171

169172
const indexedInput: IndexedInput = { index, utxo }
@@ -196,8 +199,11 @@ const resolveDeferredRedeemers = (
196199
}
197200

198201
if (batchInputs.length === 0) {
199-
yield* Effect.logWarning(`[Evaluation] Batch redeemer for ${key} has no resolved inputs`)
200-
continue
202+
return yield* Effect.fail(
203+
new TransactionBuilderError({
204+
message: `Batch redeemer for ${key} has no resolved inputs: none of the specified UTxOs are present in the transaction`
205+
})
206+
)
201207
}
202208

203209
// Sort by index for consistent ordering
@@ -559,10 +565,11 @@ export const executeEvaluation = (): Effect.Effect<
559565
// For spend redeemers, map input index to UTxO reference
560566
const utxoRef = inputIndexMapping.get(evalRedeemer.redeemer_index)
561567
if (!utxoRef) {
562-
yield* Effect.logWarning(
563-
`[Evaluation] Could not map input index ${evalRedeemer.redeemer_index} to UTxO reference`
568+
return yield* Effect.fail(
569+
new TransactionBuilderError({
570+
message: `Evaluator returned spend result at index ${evalRedeemer.redeemer_index} but no UTxO exists at that position in the transaction`
571+
})
564572
)
565-
continue
566573
}
567574

568575
const redeemer = evaluatedRedeemers.get(utxoRef)
@@ -578,14 +585,21 @@ export const executeEvaluation = (): Effect.Effect<
578585
`mem=${evalRedeemer.ex_units.mem}, steps=${evalRedeemer.ex_units.steps}`
579586
)
580587
} else {
581-
yield* Effect.logWarning(`[Evaluation] No redeemer found in state for UTxO ${utxoRef}`)
588+
return yield* Effect.fail(
589+
new TransactionBuilderError({
590+
message: `Evaluator returned spend result for ${utxoRef} but no redeemer exists in builder state for that UTxO`
591+
})
592+
)
582593
}
583594
} else if (evalRedeemer.redeemer_tag === "mint") {
584595
// For mint redeemers, map mint index to policy ID hex
585596
const policyIdHex = mintIndexMapping.get(evalRedeemer.redeemer_index)
586597
if (!policyIdHex) {
587-
yield* Effect.logWarning(`[Evaluation] Could not map mint index ${evalRedeemer.redeemer_index} to policy ID`)
588-
continue
598+
return yield* Effect.fail(
599+
new TransactionBuilderError({
600+
message: `Evaluator returned mint result at index ${evalRedeemer.redeemer_index} but no policy exists at that position in the transaction`
601+
})
602+
)
589603
}
590604

591605
const redeemer = evaluatedRedeemers.get(policyIdHex)
@@ -601,14 +615,21 @@ export const executeEvaluation = (): Effect.Effect<
601615
`mem=${evalRedeemer.ex_units.mem}, steps=${evalRedeemer.ex_units.steps}`
602616
)
603617
} else {
604-
yield* Effect.logWarning(`[Evaluation] No redeemer found in state for policy ${policyIdHex}`)
618+
return yield* Effect.fail(
619+
new TransactionBuilderError({
620+
message: `Evaluator returned mint result for policy ${policyIdHex} but no redeemer exists in builder state for that policy`
621+
})
622+
)
605623
}
606624
} else if (evalRedeemer.redeemer_tag === "cert") {
607625
// For certificate redeemers, map index to credential key
608626
const certKey = certIndexMapping.get(evalRedeemer.redeemer_index)
609627
if (!certKey) {
610-
yield* Effect.logWarning(`[Evaluation] Could not map cert index ${evalRedeemer.redeemer_index} to credential`)
611-
continue
628+
return yield* Effect.fail(
629+
new TransactionBuilderError({
630+
message: `Evaluator returned cert result at index ${evalRedeemer.redeemer_index} but no certificate exists at that position in the transaction`
631+
})
632+
)
612633
}
613634

614635
const redeemer = evaluatedRedeemers.get(certKey)
@@ -624,16 +645,21 @@ export const executeEvaluation = (): Effect.Effect<
624645
`mem=${evalRedeemer.ex_units.mem}, steps=${evalRedeemer.ex_units.steps}`
625646
)
626647
} else {
627-
yield* Effect.logWarning(`[Evaluation] No redeemer found in state for cert ${certKey}`)
648+
return yield* Effect.fail(
649+
new TransactionBuilderError({
650+
message: `Evaluator returned cert result for ${certKey} but no redeemer exists in builder state for that credential`
651+
})
652+
)
628653
}
629654
} else if (evalRedeemer.redeemer_tag === "reward") {
630655
// For withdrawal redeemers, map index to credential key
631656
const rewardKey = withdrawalIndexMapping.get(evalRedeemer.redeemer_index)
632657
if (!rewardKey) {
633-
yield* Effect.logWarning(
634-
`[Evaluation] Could not map withdrawal index ${evalRedeemer.redeemer_index} to credential`
658+
return yield* Effect.fail(
659+
new TransactionBuilderError({
660+
message: `Evaluator returned reward result at index ${evalRedeemer.redeemer_index} but no withdrawal exists at that position in the transaction`
661+
})
635662
)
636-
continue
637663
}
638664

639665
const redeemer = evaluatedRedeemers.get(rewardKey)
@@ -649,14 +675,21 @@ export const executeEvaluation = (): Effect.Effect<
649675
`mem=${evalRedeemer.ex_units.mem}, steps=${evalRedeemer.ex_units.steps}`
650676
)
651677
} else {
652-
yield* Effect.logWarning(`[Evaluation] No redeemer found in state for withdrawal ${rewardKey}`)
678+
return yield* Effect.fail(
679+
new TransactionBuilderError({
680+
message: `Evaluator returned reward result for ${rewardKey} but no redeemer exists in builder state for that withdrawal`
681+
})
682+
)
653683
}
654684
} else if (evalRedeemer.redeemer_tag === "vote") {
655685
// For vote redeemers, map index to voter key
656686
const voterKey = voteIndexMapping.get(evalRedeemer.redeemer_index)
657687
if (!voterKey) {
658-
yield* Effect.logWarning(`[Evaluation] Could not map vote index ${evalRedeemer.redeemer_index} to voter`)
659-
continue
688+
return yield* Effect.fail(
689+
new TransactionBuilderError({
690+
message: `Evaluator returned vote result at index ${evalRedeemer.redeemer_index} but no voter exists at that position in the transaction`
691+
})
692+
)
660693
}
661694

662695
const redeemer = evaluatedRedeemers.get(voterKey)
@@ -675,12 +708,20 @@ export const executeEvaluation = (): Effect.Effect<
675708
`mem=${evalRedeemer.ex_units.mem}, steps=${evalRedeemer.ex_units.steps}`
676709
)
677710
} else {
678-
yield* Effect.logWarning(`[Evaluation] No redeemer found in state for vote ${voterKey}`)
711+
return yield* Effect.fail(
712+
new TransactionBuilderError({
713+
message: `Evaluator returned vote result for ${voterKey} but no redeemer exists in builder state for that voter`
714+
})
715+
)
679716
}
680717
} else {
681-
// Unknown redeemer type
682-
yield* Effect.logWarning(
683-
`[Evaluation] Unknown redeemer type ${evalRedeemer.redeemer_tag} not yet supported for matching`
718+
// Unknown redeemer type returned by the evaluator — fail immediately.
719+
// Silently ignoring this would leave the redeemer at exUnits = 0, which
720+
// looks "unevaluated" to Balance and triggers an infinite retry loop.
721+
return yield* Effect.fail(
722+
new TransactionBuilderError({
723+
message: `Evaluator returned unknown redeemer tag "${evalRedeemer.redeemer_tag}" at index ${evalRedeemer.redeemer_index}. This is likely a provider bug or an unsupported evaluator format.`
724+
})
684725
)
685726
}
686727
}

packages/evolution/src/sdk/provider/internal/Blockfrost.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -274,9 +274,12 @@ export const transformJsonwspOgmiosEvaluationResult = (
274274
const result: Array<EvalRedeemer> = []
275275

276276
for (const [key, budget] of Object.entries(evaluationResult)) {
277-
// Parse "spend:0", "mint:1", etc.
278-
const [tag, indexStr] = key.split(":")
277+
// Parse "spend:0", "mint:1", "certificate:0", "withdrawal:0", etc.
278+
// Blockfrost uses Ogmios v5 JSONWSP which returns "certificate" and "withdrawal";
279+
// normalize to the SDK's canonical tags "cert" and "reward" (Ogmios v6 / CDDL names).
280+
const [rawTag, indexStr] = key.split(":")
279281
const index = parseInt(indexStr, 10)
282+
const tag = rawTag === "certificate" ? "cert" : rawTag === "withdrawal" ? "reward" : rawTag
280283

281284
result.push({
282285
ex_units: new Redeemer.ExUnits({

0 commit comments

Comments
 (0)