-
Notifications
You must be signed in to change notification settings - Fork 4
feat/provider full utxo resolution scripts datums #128
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
feat/provider full utxo resolution scripts datums #128
Conversation
…mios v6 Ogmios v6 with explicit JSON notation expects raw Plutus script bytes without CBOR tag envelope. Previously we were sending the full CBOR encoding [tag, bytes] which caused 'couldn't decode plutus script' errors. Now we send: - Plutus scripts: raw bytes only (script.bytes) - Native scripts: inner script structure CBOR This fixes script evaluation when using reference scripts with Kupmios provider.
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.
Pull request overview
This pull request implements comprehensive improvements to provider implementations for fetching and resolving UTxOs with complete script and datum information. The changes focus on three main areas: enhancing Blockfrost provider with pagination and full UTxO resolution, fixing Kupmios/Ogmios script encoding, and adding new transaction builder options for better control over evaluation and script data hashing.
Changes:
- Enhanced Blockfrost provider with pagination support, full UTxO resolution (scripts/datums), and improved error handling
- Fixed Plutus script CBOR encoding in Kupmios and Ogmios providers (removed double encoding, proper handling of native vs Plutus scripts)
- Added PoolKeyHash bech32 encoding/decoding support and new transaction builder options for controlling evaluator behavior
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/evolution/src/sdk/provider/internal/Ogmios.ts | Fixed Plutus script encoding to use raw bytes without CBOR envelope, added NativeScripts import for proper native script handling |
| packages/evolution/src/sdk/provider/internal/KupmiosEffects.ts | Removed unnecessary double CBOR encoding for Plutus scripts from Kupo |
| packages/evolution/src/sdk/provider/internal/HttpUtils.ts | Refactored header handling for better code organization |
| packages/evolution/src/sdk/provider/internal/BlockfrostEffect.ts | Added pagination, full UTxO resolution with script/datum fetching, improved error handling with 404 support, and switched to more reliable evaluation endpoint |
| packages/evolution/src/sdk/provider/internal/Blockfrost.ts | Updated schemas to match actual Blockfrost API responses, added Conway governance parameters, improved evaluation result handling |
| packages/evolution/src/sdk/builders/phases/Evaluation.ts | Updated comments to clarify additionalUtxos handling strategy |
| packages/evolution/src/sdk/builders/TxBuilderImpl.ts | Enhanced cost model detection to include reference scripts, added scriptDataFormat option support |
| packages/evolution/src/sdk/builders/TransactionBuilder.ts | Added passAdditionalUtxos and scriptDataFormat options to BuildOptions |
| packages/evolution/src/PoolKeyHash.ts | Implemented bech32 encoding/decoding support for pool IDs |
| .changeset/sour-bats-grin.md | Documented all changes for the release |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| }) | ||
| ) | ||
| }, | ||
| { concurrency: "unbounded" } |
Copilot
AI
Jan 12, 2026
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.
The concurrency setting "unbounded" for Effect.forEach could potentially overwhelm the Blockfrost API with concurrent requests, especially when processing transaction outputs that may have many matching UTxOs. Consider using a bounded concurrency value (e.g., { concurrency: 10 }) to match the concurrency used in getUtxos and getUtxosWithUnit functions, which helps prevent rate limiting issues and provides more predictable resource usage.
| { concurrency: "unbounded" } | |
| { concurrency: 10 } |
| ? fetchDatum(utxo.data_hash).pipe( | ||
| Effect.map((d) => d as DatumOption.DatumOption | undefined), | ||
| Effect.catchAll(() => Effect.succeed( | ||
| new DatumOption.DatumHash({ hash: Bytes.fromHex(utxo.data_hash!) }) as DatumOption.DatumOption | undefined |
Copilot
AI
Jan 12, 2026
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.
Multiple uses of the non-null assertion operator (!) on utxo.data_hash at lines 259, 354, and 447. While these are guarded by conditional checks, using the non-null assertion operator can be risky and may mask potential issues. Consider refactoring to use the already-checked variable or explicit type narrowing to avoid the need for the non-null assertion.
| const datumEffect = utxo.inline_datum | ||
| ? Effect.succeed( | ||
| new DatumOption.InlineDatum({ | ||
| data: PlutusData.fromCBORHex(utxo.inline_datum) | ||
| }) as DatumOption.DatumOption | undefined | ||
| ) | ||
| : utxo.data_hash | ||
| ? fetchDatum(utxo.data_hash).pipe( | ||
| Effect.map((d) => d as DatumOption.DatumOption | undefined), | ||
| Effect.catchAll(() => Effect.succeed( | ||
| new DatumOption.DatumHash({ hash: Bytes.fromHex(utxo.data_hash!) }) as DatumOption.DatumOption | undefined |
Copilot
AI
Jan 12, 2026
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.
Multiple uses of the non-null assertion operator (!) on utxo.data_hash. While guarded by conditional checks, using the non-null assertion operator can be risky and may mask potential issues. Consider refactoring to use the already-checked variable or explicit type narrowing to avoid the need for the non-null assertion.
| const datumEffect = utxo.inline_datum | |
| ? Effect.succeed( | |
| new DatumOption.InlineDatum({ | |
| data: PlutusData.fromCBORHex(utxo.inline_datum) | |
| }) as DatumOption.DatumOption | undefined | |
| ) | |
| : utxo.data_hash | |
| ? fetchDatum(utxo.data_hash).pipe( | |
| Effect.map((d) => d as DatumOption.DatumOption | undefined), | |
| Effect.catchAll(() => Effect.succeed( | |
| new DatumOption.DatumHash({ hash: Bytes.fromHex(utxo.data_hash!) }) as DatumOption.DatumOption | undefined | |
| const dataHash = utxo.data_hash | |
| const datumEffect = utxo.inline_datum | |
| ? Effect.succeed( | |
| new DatumOption.InlineDatum({ | |
| data: PlutusData.fromCBORHex(utxo.inline_datum) | |
| }) as DatumOption.DatumOption | undefined | |
| ) | |
| : dataHash | |
| ? fetchDatum(dataHash).pipe( | |
| Effect.map((d) => d as DatumOption.DatumOption | undefined), | |
| Effect.catchAll(() => Effect.succeed( | |
| new DatumOption.DatumHash({ hash: Bytes.fromHex(dataHash) }) as DatumOption.DatumOption | undefined |
| ? fetchDatum(utxo.data_hash).pipe( | ||
| Effect.map((d) => d as DatumOption.DatumOption | undefined), | ||
| Effect.catchAll(() => Effect.succeed( | ||
| new DatumOption.DatumHash({ hash: Bytes.fromHex(utxo.data_hash!) }) as DatumOption.DatumOption | undefined | ||
| )) | ||
| ) |
Copilot
AI
Jan 12, 2026
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.
Multiple uses of the non-null assertion operator (!) on utxo.data_hash. While guarded by conditional checks, using the non-null assertion operator can be risky and may mask potential issues. Consider refactoring to use the already-checked variable or explicit type narrowing to avoid the need for the non-null assertion.
| ? fetchDatum(utxo.data_hash).pipe( | |
| Effect.map((d) => d as DatumOption.DatumOption | undefined), | |
| Effect.catchAll(() => Effect.succeed( | |
| new DatumOption.DatumHash({ hash: Bytes.fromHex(utxo.data_hash!) }) as DatumOption.DatumOption | undefined | |
| )) | |
| ) | |
| ? (() => { | |
| const dataHash = utxo.data_hash | |
| return fetchDatum(dataHash).pipe( | |
| Effect.map((d) => d as DatumOption.DatumOption | undefined), | |
| Effect.catchAll(() => | |
| Effect.succeed( | |
| new DatumOption.DatumHash({ hash: Bytes.fromHex(dataHash) }) as DatumOption.DatumOption | undefined | |
| ) | |
| ) | |
| ) | |
| })() |
| Eff.gen(function* () { | ||
| const result = yield* Eff.try({ | ||
| try: () => { | ||
| const decoded = bech32.decode(fromA as any, false) |
Copilot
AI
Jan 12, 2026
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.
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.
| const decoded = bech32.decode(fromA as any, false) | |
| const decoded = bech32.decode(fromA, false) |
| return new Uint8Array(bytes) | ||
| }, | ||
| catch: () => new ParseResult.Type(ast, fromA, `Failed to decode Bech32 pool id: ${fromA}`) |
Copilot
AI
Jan 12, 2026
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.
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.
| 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}`) |
| : output.data_hash | ||
| ? fetchDatum(output.data_hash).pipe( | ||
| Effect.map((d) => d as DatumOption.DatumOption | undefined), | ||
| Effect.catchAll(() => Effect.succeed(undefined)) |
Copilot
AI
Jan 12, 2026
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.
When fetching a datum by hash fails, the code falls back to creating a DatumHash with the original hash. However, the fallback in the getUtxosByOutRef function at line 518 returns undefined instead of falling back to DatumHash like in other functions (e.g., getUtxos at line 259, getUtxosWithUnit at line 354, getUtxoByUnit at line 447). This inconsistency means that if datum resolution fails for outputs referenced in getUtxosByOutRef, the datum information is completely lost rather than preserved as a hash reference. Consider using the same fallback pattern across all UTxO resolution functions for consistency.
| Effect.catchAll(() => Effect.succeed(undefined)) | |
| Effect.catchAll(() => | |
| Effect.succeed( | |
| new DatumOption.DatumHash({ hash: output.data_hash! }) as DatumOption.DatumOption | |
| ) | |
| ) |
No description provided.