Skip to content
5 changes: 5 additions & 0 deletions .changeset/fix-provider-error-handling.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@evolution-sdk/evolution": patch
---

Fix Blockfrost evaluateTx failing on multi-asset UTxOs by correcting the value format sent to the Ogmios endpoint. Standardize error handling across all providers with consistent catchAll + wrapError pattern. Add JSONWSP fault detection to Blockfrost evaluation responses. Accept both CBOR tag-258 and plain array encodings in TransactionBody decoding.
77 changes: 43 additions & 34 deletions packages/evolution/src/TransactionBody.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,8 @@ const decodeAuxiliaryDataHash = ParseResult.decodeEither(AuxiliaryDataHash.FromB
const decodeScriptDataHash = ParseResult.decodeEither(ScriptDataHash.FromBytes)
const decodeKeyHash = ParseResult.decodeEither(KeyHash.FromBytes)

const decodeInputs = ParseResult.decodeUnknownEither(CBOR.tag(258, Schema.Array(TransactionInput.FromCDDL)))
const decodeTaggedInputs = ParseResult.decodeUnknownEither(CBOR.tag(258, Schema.Array(TransactionInput.FromCDDL)))
const decodeUntaggedInputs = ParseResult.decodeUnknownEither(Schema.Array(TransactionInput.FromCDDL))

/**
* CDDL schema for TransactionBody struct structure.
Expand Down Expand Up @@ -314,10 +315,12 @@ export const FromCDDL = Schema.transformOrFail(CDDLSchema, Schema.typeSchema(Tra
}),
decode: (fromA) =>
E.gen(function* () {
// Required fields - access via helper
const inputsTag = fromA.get(0n)
const decodedInputs = yield* decodeInputs(inputsTag)
const inputs = decodedInputs.value
// Required fields - accept both tag-258 (Conway) and plain array (Babbage)
const inputsRaw = fromA.get(0n)
const taggedResult = decodeTaggedInputs(inputsRaw)
const inputs = E.isRight(taggedResult)
? taggedResult.right.value
: yield* decodeUntaggedInputs(inputsRaw)

// const inputsArray = inputsTag.value
// const inputsLen = inputsArray.length
Expand Down Expand Up @@ -370,14 +373,16 @@ export const FromCDDL = Schema.transformOrFail(CDDLSchema, Schema.typeSchema(Tra
const scriptDataHashBytes = fromA.get(11n) as Uint8Array | undefined
const scriptDataHash = scriptDataHashBytes ? yield* decodeScriptDataHash(scriptDataHashBytes) : undefined

const collateralInputsTag = fromA.get(13n) as
| {
_tag: "Tag"
tag: 258
value: ReadonlyArray<typeof TransactionInput.CDDLSchema.Type>
}
// Accept both tag-258 (Conway) and plain array (Babbage) for collateral inputs
const collateralInputsRaw = fromA.get(13n) as
| { _tag: "Tag"; tag: 258; value: ReadonlyArray<typeof TransactionInput.CDDLSchema.Type> }
| ReadonlyArray<typeof TransactionInput.CDDLSchema.Type>
| undefined
const collateralInputsArray = collateralInputsTag ? collateralInputsTag.value : undefined
const collateralInputsArray = collateralInputsRaw
? (collateralInputsRaw as any)._tag === "Tag"
? (collateralInputsRaw as any).value
: collateralInputsRaw
: undefined
let collateralInputs: NonEmptyArray<TransactionInput.TransactionInput> | undefined
if (collateralInputsArray) {
const len = collateralInputsArray.length
Expand All @@ -388,14 +393,16 @@ export const FromCDDL = Schema.transformOrFail(CDDLSchema, Schema.typeSchema(Tra
collateralInputs = arr as NonEmptyArray<TransactionInput.TransactionInput>
}

const requiredSignersTag = fromA.get(14n) as
| {
_tag: "Tag"
tag: 258
value: ReadonlyArray<Uint8Array>
}
// Accept both tag-258 (Conway) and plain array (Babbage) for required signers
const requiredSignersRaw = fromA.get(14n) as
| { _tag: "Tag"; tag: 258; value: ReadonlyArray<Uint8Array> }
| ReadonlyArray<Uint8Array>
| undefined
const requiredSignersArray = requiredSignersTag ? requiredSignersTag.value : undefined
const requiredSignersArray = requiredSignersRaw
? (requiredSignersRaw as any)._tag === "Tag"
? (requiredSignersRaw as any).value
: requiredSignersRaw
: undefined
let requiredSigners: NonEmptyArray<KeyHash.KeyHash> | undefined
if (requiredSignersArray) {
const len = requiredSignersArray.length
Expand All @@ -411,14 +418,16 @@ export const FromCDDL = Schema.transformOrFail(CDDLSchema, Schema.typeSchema(Tra
const collateralReturn = collateralReturnData ? yield* decodeTxOutput(collateralReturnData) : undefined
const totalCollateral = fromA.get(17n) as Coin.Coin | undefined

const referenceInputsTag = fromA.get(18n) as
| {
_tag: "Tag"
tag: 258
value: ReadonlyArray<typeof TransactionInput.CDDLSchema.Type>
}
// Accept both tag-258 (Conway) and plain array (Babbage) for reference inputs
const referenceInputsRaw = fromA.get(18n) as
| { _tag: "Tag"; tag: 258; value: ReadonlyArray<typeof TransactionInput.CDDLSchema.Type> }
| ReadonlyArray<typeof TransactionInput.CDDLSchema.Type>
| undefined
const referenceInputsArray = referenceInputsTag ? referenceInputsTag.value : undefined
const referenceInputsArray: ReadonlyArray<typeof TransactionInput.CDDLSchema.Type> | undefined = referenceInputsRaw
? (referenceInputsRaw as any)._tag === "Tag"
? (referenceInputsRaw as any).value
: referenceInputsRaw
: undefined
let referenceInputs: NonEmptyArray<TransactionInput.TransactionInput> | undefined
if (referenceInputsArray) {
const len = referenceInputsArray.length
Expand All @@ -430,15 +439,15 @@ export const FromCDDL = Schema.transformOrFail(CDDLSchema, Schema.typeSchema(Tra
}
const votingProceduresData = fromA.get(19n) as typeof VotingProcedures.CDDLSchema.Type | undefined
const votingProcedures = votingProceduresData ? yield* decodeVotingProcedures(votingProceduresData) : undefined
const proposalProceduresTag = fromA.get(20n) as
| {
_tag: "Tag"
tag: 258
value: ReadonlyArray<typeof ProposalProcedure.CDDLSchema.Type>
}
// Accept both tag-258 (Conway) and plain array (Babbage) for proposal procedures
const proposalProceduresRaw = fromA.get(20n) as
| { _tag: "Tag"; tag: 258; value: ReadonlyArray<typeof ProposalProcedure.CDDLSchema.Type> }
| ReadonlyArray<typeof ProposalProcedure.CDDLSchema.Type>
| undefined
const proposalProceduresArray = proposalProceduresTag
? (proposalProceduresTag.value as ReadonlyArray<typeof ProposalProcedure.CDDLSchema.Type>)
const proposalProceduresArray: ReadonlyArray<typeof ProposalProcedure.CDDLSchema.Type> | undefined = proposalProceduresRaw
? (proposalProceduresRaw as any)._tag === "Tag"
? (proposalProceduresRaw as any).value
: proposalProceduresRaw
: undefined
const proposalProcedures = proposalProceduresArray
? new ProposalProcedures.ProposalProcedures({
Expand Down
50 changes: 43 additions & 7 deletions packages/evolution/src/sdk/provider/internal/Blockfrost.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@
* Internal module for Blockfrost provider implementation
*/

import { Schema } from "effect"
import { Effect, Schema } from "effect"

import * as CoreAssets from "../../../Assets/index.js"
import * as PoolKeyHash from "../../../PoolKeyHash.js"
import * as Redeemer from "../../../Redeemer.js"
import type { EvalRedeemer } from "../../EvalRedeemer.js"
import type * as Provider from "../Provider.js"
import { ProviderError } from "../Provider.js"

// ============================================================================
// Blockfrost API Response Schemas
Expand Down Expand Up @@ -158,7 +159,7 @@ export const JsonwspOgmiosEvaluationResponse = Schema.Struct({
version: Schema.optional(Schema.String),
servicename: Schema.optional(Schema.String),
methodname: Schema.optional(Schema.String),
result: Schema.Struct({
result: Schema.optional(Schema.Struct({
EvaluationResult: Schema.optional(
Schema.Record({
key: Schema.String, // "spend:0", "mint:1", etc.
Expand All @@ -169,7 +170,11 @@ export const JsonwspOgmiosEvaluationResponse = Schema.Struct({
})
),
EvaluationFailure: Schema.optional(Schema.Unknown)
}),
})),
fault: Schema.optional(Schema.Struct({
code: Schema.optional(Schema.String),
string: Schema.optional(Schema.String)
})),
reflection: Schema.optional(Schema.Unknown)
})

Expand Down Expand Up @@ -258,17 +263,48 @@ export const transformDelegation = (blockfrostDelegation: BlockfrostDelegation):
*/
export const transformJsonwspOgmiosEvaluationResult = (
jsonwspResponse: JsonwspOgmiosEvaluationResponse
): Array<EvalRedeemer> => {
): Effect.Effect<Array<EvalRedeemer>, ProviderError> => {
// Handle JSONWSP fault response (Ogmios backend error)
if (jsonwspResponse.type === "jsonwsp/fault") {
const faultMessage = jsonwspResponse.fault?.string ?? "unknown fault"
return Effect.fail(
new ProviderError({
message: `Blockfrost evaluation fault: ${faultMessage}`,
cause: jsonwspResponse
})
)
}

// Handle missing result field
if (!jsonwspResponse.result) {
return Effect.fail(
new ProviderError({
message: `Blockfrost evaluation returned no result`,
cause: jsonwspResponse
})
)
}

// Check for evaluation failure
if (jsonwspResponse.result.EvaluationFailure) {
const failure = jsonwspResponse.result.EvaluationFailure
throw new Error(`Script evaluation failed: ${JSON.stringify(failure)}`)
return Effect.fail(
new ProviderError({
message: `Blockfrost script evaluation failed`,
cause: failure
})
)
}

// Handle success case
const evaluationResult = jsonwspResponse.result.EvaluationResult
if (!evaluationResult) {
throw new Error("No evaluation result returned from Blockfrost")
return Effect.fail(
new ProviderError({
message: `Blockfrost evaluation returned no result`,
cause: "No EvaluationResult in response"
})
)
}

const result: Array<EvalRedeemer> = []
Expand All @@ -291,5 +327,5 @@ export const transformJsonwspOgmiosEvaluationResult = (
})
}

return result
return Effect.succeed(result)
}
Loading
Loading