Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .changeset/sour-bats-grin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
"@evolution-sdk/evolution": patch
---

### Provider Improvements: Full UTxO Resolution with Scripts and Datums

**Blockfrost Provider:**
- Added pagination support for `getUtxos` and `getUtxosWithUnit` (handles addresses with >100 UTxOs)
- Full UTxO resolution now fetches reference scripts and resolves datum hashes
- Updated `BlockfrostDelegation` schema to match actual `/accounts/{stake_address}` endpoint response
- Added `BlockfrostAssetAddress` and `BlockfrostTxUtxos` schemas for proper endpoint handling
- Improved `evaluateTx` to always use the more reliable `/utils/txs/evaluate/utxos` JSON endpoint
- Added `EvaluationFailure` handling in evaluation response schema
- Fixed delegation transformation to use `withdrawable_amount` for rewards
- Added Conway era governance parameters (`drep_deposit`, `gov_action_deposit`) to protocol params

**Kupmios Provider:**
- Removed unnecessary double CBOR encoding for Plutus scripts (Kupo returns properly encoded scripts)

**PoolKeyHash:**
- Added `FromBech32` schema for parsing pool IDs in bech32 format (pool1...)
- Added `fromBech32` and `toBech32` helper functions

**Transaction Builder:**
- Added `passAdditionalUtxos` option to control UTxO passing to provider evaluators (default: false to avoid OverlappingAdditionalUtxo errors)
- Added `scriptDataFormat` option to choose between Conway-era array format and Babbage-era map format for redeemers
- Fixed cost model detection to check reference scripts (not just witness set scripts) for Plutus version detection
4 changes: 2 additions & 2 deletions packages/evolution-devnet/src/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -720,7 +720,7 @@ export const DEFAULT_KUPO_CONFIG: Required<KupoConfig> = {
*/
export const DEFAULT_OGMIOS_CONFIG: Required<OgmiosConfig> = {
enabled: true,
image: "cardanosolutions/ogmios:v6.12.0",
image: "cardanosolutions/ogmios:v6.14.0",
port: 1337,
logLevel: "info"
} as const
Expand All @@ -734,7 +734,7 @@ export const DEFAULT_OGMIOS_CONFIG: Required<OgmiosConfig> = {
*/
export const DEFAULT_DEVNET_CONFIG: Required<DevNetConfig> = {
clusterName: "devnet",
image: "ghcr.io/intersectmbo/cardano-node:10.4.1",
image: "ghcr.io/intersectmbo/cardano-node:10.5.1",
ports: {
node: 4001,
submit: 8090
Expand Down
36 changes: 24 additions & 12 deletions packages/evolution-devnet/test/TxBuilder.Scripts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,8 @@ describe("TxBuilder Script Handling", () => {
const signBuilder = await builder.build({
changeAddress: CoreAddress.fromBech32(CHANGE_ADDRESS),
availableUtxos: [fundingUtxo],
protocolParameters: PROTOCOL_PARAMS
protocolParameters: PROTOCOL_PARAMS,
passAdditionalUtxos: true // Required for synthetic UTxOs not on chain
})

const tx = await signBuilder.toTransaction()
Expand Down Expand Up @@ -388,7 +389,8 @@ describe("TxBuilder Script Handling", () => {
const signBuilder = await builder.build({
changeAddress: CoreAddress.fromBech32(CHANGE_ADDRESS),
availableUtxos: [multiAssetUtxo],
protocolParameters: PROTOCOL_PARAMS
protocolParameters: PROTOCOL_PARAMS,
passAdditionalUtxos: true // Required for synthetic UTxOs not on chain
})

const tx = await signBuilder.toTransaction()
Expand Down Expand Up @@ -600,7 +602,8 @@ describe("TxBuilder Script Handling", () => {
const signBuilder = await builder.build({
changeAddress: CoreAddress.fromBech32(CHANGE_ADDRESS),
availableUtxos: [utxoWithRefScript, utxoWithoutRefScript],
protocolParameters: PROTOCOL_PARAMS
protocolParameters: PROTOCOL_PARAMS,
passAdditionalUtxos: true // Required for synthetic UTxOs not on chain
})

const tx = await signBuilder.toTransaction()
Expand Down Expand Up @@ -674,7 +677,8 @@ describe("TxBuilder Script Handling", () => {
const signBuilder = await builder.build({
changeAddress: CoreAddress.fromBech32(CHANGE_ADDRESS),
availableUtxos: [tightFundingUtxo, collateralUtxo],
protocolParameters: PROTOCOL_PARAMS
protocolParameters: PROTOCOL_PARAMS,
passAdditionalUtxos: true // Required for synthetic UTxOs not on chain
})

const tx = await signBuilder.toTransaction()
Expand Down Expand Up @@ -755,7 +759,8 @@ describe("TxBuilder Script Handling", () => {
const signBuilder = await builder.build({
changeAddress: CoreAddress.fromBech32(CHANGE_ADDRESS),
availableUtxos: [collateralUtxo],
protocolParameters: PROTOCOL_PARAMS
protocolParameters: PROTOCOL_PARAMS,
passAdditionalUtxos: true // Required for synthetic UTxOs not on chain
})

const tx = await signBuilder.toTransaction()
Expand Down Expand Up @@ -816,7 +821,8 @@ describe("TxBuilder Script Handling", () => {
const signBuilder = await builder.build({
changeAddress: CoreAddress.fromBech32(CHANGE_ADDRESS),
availableUtxos: [],
protocolParameters: PROTOCOL_PARAMS
protocolParameters: PROTOCOL_PARAMS,
passAdditionalUtxos: true // Required for synthetic UTxOs not on chain
})

const tx = await signBuilder.toTransaction()
Expand Down Expand Up @@ -863,7 +869,8 @@ describe("TxBuilder Script Handling", () => {
const signBuilder = await builder.build({
changeAddress: CoreAddress.fromBech32(CHANGE_ADDRESS),
availableUtxos: [],
protocolParameters: PROTOCOL_PARAMS
protocolParameters: PROTOCOL_PARAMS,
passAdditionalUtxos: true // Required for synthetic UTxOs not on chain
})

const tx = await signBuilder.toTransaction()
Expand Down Expand Up @@ -908,7 +915,8 @@ describe("TxBuilder Script Handling", () => {
const signBuilder = await builder.build({
changeAddress: CoreAddress.fromBech32(CHANGE_ADDRESS),
availableUtxos: [],
protocolParameters: PROTOCOL_PARAMS
protocolParameters: PROTOCOL_PARAMS,
passAdditionalUtxos: true // Required for synthetic UTxOs not on chain
})

const tx = await signBuilder.toTransaction()
Expand Down Expand Up @@ -998,7 +1006,8 @@ describe("TxBuilder Script Handling", () => {
const signBuilder = await builder.build({
changeAddress: CoreAddress.fromBech32(CHANGE_ADDRESS),
availableUtxos: [],
protocolParameters: PROTOCOL_PARAMS
protocolParameters: PROTOCOL_PARAMS,
passAdditionalUtxos: true // Required for synthetic UTxOs not on chain
})

const tx = await signBuilder.toTransaction()
Expand Down Expand Up @@ -1040,7 +1049,8 @@ describe("TxBuilder Script Handling", () => {
const signBuilder = await builder.build({
changeAddress: CoreAddress.fromBech32(CHANGE_ADDRESS),
availableUtxos: [],
protocolParameters: PROTOCOL_PARAMS
protocolParameters: PROTOCOL_PARAMS,
passAdditionalUtxos: true // Required for synthetic UTxOs not on chain
})

const tx = await signBuilder.toTransaction()
Expand Down Expand Up @@ -1120,7 +1130,8 @@ describe("TxBuilder Script Handling", () => {
const signBuilder = await builder.build({
changeAddress: CoreAddress.fromBech32(CHANGE_ADDRESS),
availableUtxos: [collateralUtxo1, collateralUtxo2, collateralUtxo3, collateralUtxo4],
protocolParameters: PROTOCOL_PARAMS
protocolParameters: PROTOCOL_PARAMS,
passAdditionalUtxos: true // Required for synthetic UTxOs not on chain
})

const tx = await signBuilder.toTransaction()
Expand Down Expand Up @@ -1195,7 +1206,8 @@ describe("TxBuilder Script Handling", () => {
const signBuilder = await builder.build({
changeAddress: CoreAddress.fromBech32(CHANGE_ADDRESS),
availableUtxos: [collateralUtxo1, collateralUtxo2, collateralUtxo3],
protocolParameters: PROTOCOL_PARAMS
protocolParameters: PROTOCOL_PARAMS,
passAdditionalUtxos: true // Required for synthetic UTxOs not on chain
})

const tx = await signBuilder.toTransaction()
Expand Down
2 changes: 2 additions & 0 deletions packages/evolution/src/Address.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,8 @@ export const FromBech32 = Schema.transformOrFail(Schema.String, Schema.typeSchem
Eff.gen(function* () {
const result = yield* Eff.try({
try: () => {
// Note: `as any` needed because bech32.decode expects template literal type `${Prefix}1${string}`
// but Schema provides plain string. Consider using decodeToBytes which accepts string.
const decoded = bech32.decode(fromA as any, false)
const bytes = bech32.fromWords(decoded.words)
return new Uint8Array(bytes)
Expand Down
2 changes: 2 additions & 0 deletions packages/evolution/src/AddressEras.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,8 @@ export const FromBech32 = Schema.transformOrFail(Schema.String, Schema.typeSchem
Eff.gen(function* () {
const result = yield* Eff.try({
try: () => {
// Note: `as any` needed because bech32.decode expects template literal type `${Prefix}1${string}`
// but Schema provides plain string. Consider using decodeToBytes which accepts string.
const decoded = bech32.decode(fromA as any, false)
const bytes = bech32.fromWords(decoded.words)
return new Uint8Array(bytes)
Expand Down
2 changes: 2 additions & 0 deletions packages/evolution/src/CommitteeColdCredential.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,8 @@ export const FromBech32 = Schema.transformOrFail(Schema.String, Schema.typeSchem
Effect.gen(function* () {
const result = yield* Effect.try({
try: () => {
// Note: `as any` needed because bech32.decode expects template literal type `${Prefix}1${string}`
// but Schema provides plain string. Consider using decodeToBytes which accepts string.
const decoded = bech32.decode(fromA as any, false)
if (decoded.prefix !== "cc_cold") {
throw new Error(`Invalid prefix: expected "cc_cold", got "${decoded.prefix}"`)
Expand Down
2 changes: 2 additions & 0 deletions packages/evolution/src/CommitteeHotCredential.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,8 @@ export const FromBech32 = Schema.transformOrFail(Schema.String, Schema.typeSchem
Effect.gen(function* () {
const result = yield* Effect.try({
try: () => {
// Note: `as any` needed because bech32.decode expects template literal type `${Prefix}1${string}`
// but Schema provides plain string. Consider using decodeToBytes which accepts string.
const decoded = bech32.decode(fromA as any, false)
if (decoded.prefix !== "cc_hot") {
throw new Error(`Invalid prefix: expected "cc_hot", got "${decoded.prefix}"`)
Expand Down
2 changes: 2 additions & 0 deletions packages/evolution/src/DRep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,8 @@ export const FromBech32 = Schema.transformOrFail(Schema.String, Schema.typeSchem
Eff.gen(function* () {
const result = yield* Eff.try({
try: () => {
// Note: `as any` needed because bech32.decode expects template literal type `${Prefix}1${string}`
// but Schema provides plain string. Consider using decodeToBytes which accepts string.
const decoded = bech32.decode(fromA as any, false)
if (decoded.prefix !== "drep") {
throw new Error(`Invalid prefix: expected "drep", got "${decoded.prefix}"`)
Expand Down
51 changes: 50 additions & 1 deletion packages/evolution/src/PoolKeyHash.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Equal, FastCheck, Hash, Inspectable, Schema } from "effect"
import { bech32 } from "@scure/base"
import { Effect as Eff, Equal, FastCheck, Hash, Inspectable, ParseResult, Schema } from "effect"

import * as Bytes from "./Bytes.js"
import * as Hash28 from "./Hash28.js"
Expand Down Expand Up @@ -59,6 +60,38 @@ export const FromHex = Schema.compose(Hash28.BytesFromHex, FromBytes).annotation
identifier: "PoolKeyHash.FromHex"
})

/**
* Schema transformer from bech32 string (pool1...) to PoolKeyHash.
*
* @since 2.0.0
* @category schemas
*/
export const FromBech32 = Schema.transformOrFail(Schema.String, Schema.typeSchema(PoolKeyHash), {
strict: true,
encode: (poolKeyHash) =>
Eff.gen(function* () {
const words = bech32.toWords(poolKeyHash.hash)
return bech32.encode("pool", words, false)
}),
decode: (fromA, _, ast) =>
Eff.gen(function* () {
const result = yield* Eff.try({
try: () => {
// Note: `as any` needed because bech32.decode expects template literal type `${Prefix}1${string}`
// but Schema provides plain string. Consider using decodeToBytes which accepts string.
const decoded = bech32.decode(fromA as any, false)
Copy link

Copilot AI Jan 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type assertion as any is used here to bypass type checking for the bech32.decode function. This is a code smell that could hide potential type mismatches. Consider removing the type assertion and ensuring the input string type matches the expected signature of bech32.decode, or add a proper type guard if needed.

Suggested change
const decoded = bech32.decode(fromA as any, false)
const decoded = bech32.decode(fromA, false)

Copilot uses AI. Check for mistakes.
const bytes = bech32.fromWords(decoded.words)
return new Uint8Array(bytes)
},
catch: () => new ParseResult.Type(ast, fromA, `Failed to decode Bech32 pool id: ${fromA}`)
Comment on lines +84 to +86
Copy link

Copilot AI Jan 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The decoded bech32 bytes should be validated to ensure they match the expected length for a PoolKeyHash (28 bytes, as defined by Hash28.BYTES_LENGTH). Without this check, invalid pool IDs with incorrect byte lengths could pass through validation, potentially causing issues downstream when the PoolKeyHash is used.

Suggested change
return new Uint8Array(bytes)
},
catch: () => new ParseResult.Type(ast, fromA, `Failed to decode Bech32 pool id: ${fromA}`)
if (bytes.length !== Hash28.BYTES_LENGTH) {
throw new ParseResult.Type(
ast,
fromA,
`Invalid PoolKeyHash length: expected ${Hash28.BYTES_LENGTH} bytes, got ${bytes.length}`
)
}
return new Uint8Array(bytes)
},
catch: (error) =>
error instanceof ParseResult.Type
? error
: new ParseResult.Type(ast, fromA, `Failed to decode Bech32 pool id: ${fromA}`)

Copilot uses AI. Check for mistakes.
})
return yield* ParseResult.decode(FromBytes)(result)
})
}).annotations({
identifier: "PoolKeyHash.FromBech32",
description: "Transforms Bech32 pool id string to PoolKeyHash"
})

/**
* FastCheck arbitrary for generating random PoolKeyHash instances.
*
Expand Down Expand Up @@ -105,3 +138,19 @@ export const toBytes = Schema.encodeSync(FromBytes)
* @category encoding
*/
export const toHex = Schema.encodeSync(FromHex)

/**
* Parse PoolKeyHash from bech32 string (pool1...).
*
* @since 2.0.0
* @category parsing
*/
export const fromBech32 = Schema.decodeSync(FromBech32)

/**
* Encode PoolKeyHash to bech32 string (pool1...).
*
* @since 2.0.0
* @category encoding
*/
export const toBech32 = Schema.encodeSync(FromBech32)
2 changes: 2 additions & 0 deletions packages/evolution/src/PrivateKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ export const FromBech32 = Schema.transformOrFail(Schema.String, Schema.typeSchem
decode: (fromA, _, ast) =>
E.gen(function* () {
const { prefix, words } = yield* ParseResult.try({
// Note: `as any` needed because bech32.decode expects template literal type `${Prefix}1${string}`
// but Schema provides plain string. Consider using decodeToBytes which accepts string.
try: () => bech32.decode(fromA as any, 1023),
catch: (error) => new ParseResult.Type(ast, fromA, `Failed to decode bech32 string: ${(error as Error).message}`)
})
Expand Down
2 changes: 2 additions & 0 deletions packages/evolution/src/RewardAccount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ export const FromBech32 = Schema.transformOrFail(Schema.String, Schema.typeSchem
Eff.gen(function* () {
const result = yield* Eff.try({
try: () => {
// Note: `as any` needed because bech32.decode expects template literal type `${Prefix}1${string}`
// but Schema provides plain string. Consider using decodeToBytes which accepts string.
const decoded = bech32.decode(fromA as any, false)
const bytes = bech32.fromWords(decoded.words)
return new Uint8Array(bytes)
Expand Down
41 changes: 39 additions & 2 deletions packages/evolution/src/sdk/builders/TransactionBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -381,8 +381,14 @@ const resolveEvaluator = (config: TxBuilderConfig, options?: BuildOptions): Eval
additionalUtxos: ReadonlyArray<CoreUTxO.UTxO> | undefined,
_context: EvaluationContext
) => {
// Provider now accepts Transaction directly
return config.provider!.Effect.evaluateTx(tx, additionalUtxos as Array<CoreUTxO.UTxO> | undefined).pipe(
// Provider-based evaluators (Ogmios, Blockfrost) resolve UTxOs from chain.
// By default, don't pass additionalUtxos to avoid OverlappingAdditionalUtxo errors.
// Use passAdditionalUtxos: true for edge cases (e.g., UTxOs not yet on chain).
const utxosToPass = options?.passAdditionalUtxos
? (additionalUtxos as Array<CoreUTxO.UTxO> | undefined)
: undefined

return config.provider!.Effect.evaluateTx(tx, utxosToPass).pipe(
Effect.mapError((providerError) => {
// Parse provider error into structured failures
const failures = parseProviderError(providerError)
Expand Down Expand Up @@ -1030,6 +1036,37 @@ export interface BuildOptions {
*/
readonly evaluator?: Evaluator

/**
* Pass additional UTxOs to provider-based evaluators.
*
* By default, provider evaluators (Ogmios, Blockfrost) don't receive additionalUtxos
* because they can resolve UTxOs from the chain, and passing them causes
* "OverlappingAdditionalUtxo" errors.
*
* Set to `true` for edge cases where you need to evaluate with UTxOs that
* are not yet on chain (e.g., chained transactions, emulator scenarios).
*
* Note: This option has no effect on custom evaluators (Aiken, Scalus) which
* always receive additionalUtxos since they cannot resolve from chain.
*
* @default false
* @since 2.0.0
*/
readonly passAdditionalUtxos?: boolean

/**
* Format for encoding redeemers in the script data hash.
*
* - `"array"` (DEFAULT): Conway-era format, redeemers encoded as array
* - `"map"`: Babbage-era format, redeemers encoded as map
*
* Use `"map"` for Babbage compatibility or debugging.
*
* @default "array"
* @since 2.0.0
*/
readonly scriptDataFormat?: "array" | "map"

/**
* Custom slot configuration for script evaluation.
*
Expand Down
30 changes: 25 additions & 5 deletions packages/evolution/src/sdk/builders/TxBuilderImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -670,9 +670,27 @@ export const assembleTransaction = (

// Only include cost models for Plutus versions actually used in the transaction
// The scriptDataHash must use the same languages as the node will compute
const hasPlutusV1 = plutusV1Scripts.length > 0
const hasPlutusV2 = plutusV2Scripts.length > 0
const hasPlutusV3 = plutusV3Scripts.length > 0
// Check both witness set scripts AND reference scripts
let hasPlutusV1 = plutusV1Scripts.length > 0
let hasPlutusV2 = plutusV2Scripts.length > 0
let hasPlutusV3 = plutusV3Scripts.length > 0

// Also check reference inputs for Plutus scripts
for (const refUtxo of state.referenceInputs) {
if (refUtxo.scriptRef) {
switch (refUtxo.scriptRef._tag) {
case "PlutusV1":
hasPlutusV1 = true
break
case "PlutusV2":
hasPlutusV2 = true
break
case "PlutusV3":
hasPlutusV3 = true
break
}
}
}

const plutusV1Costs = hasPlutusV1
? Object.values(fullProtocolParams.costModels.PlutusV1).map((v) => BigInt(v))
Expand All @@ -693,8 +711,10 @@ export const assembleTransaction = (
})

// Compute the hash of script data (redeemers + optional datums + cost models)
scriptDataHash = hashScriptData(redeemers, costModels, plutusDataArray.length > 0 ? plutusDataArray : undefined)
yield* Effect.logDebug(`[Assembly] Computed scriptDataHash: ${scriptDataHash.hash.toString()}`)
const buildOpts = yield* BuildOptionsTag
const scriptDataFmt = buildOpts.scriptDataFormat ?? "array"
scriptDataHash = hashScriptData(redeemers, costModels, plutusDataArray.length > 0 ? plutusDataArray : undefined, scriptDataFmt)
yield* Effect.logDebug(`[Assembly] Computed scriptDataHash (format=${scriptDataFmt}): ${scriptDataHash.hash.toString()}`)
}

yield* Effect.logDebug(`[Assembly] WitnessSet populated:`)
Expand Down
Loading