From 684cbaf706b82554c87370146847b7b4a9b3ae2e Mon Sep 17 00:00:00 2001 From: Mavis2103 Date: Sun, 26 Apr 2026 14:04:13 +0700 Subject: [PATCH 1/4] feat(provider): implement fullProtocolParameters support for transaction builds --- .../src/sdk/builders/TransactionBuilder.ts | 28 +- .../src/sdk/builders/internal/resolve.ts | 13 + .../src/sdk/builders/internal/txBuilder.ts | 56 ++-- .../src/sdk/builders/operations/Governance.ts | 78 +++-- .../src/sdk/builders/operations/Pool.ts | 40 ++- .../src/sdk/builders/operations/Propose.ts | 38 +-- .../src/sdk/builders/operations/Stake.ts | 113 ++++--- .../src/sdk/builders/phases/Evaluation.ts | 57 ++-- .../TxBuilder.ProtocolParamsOverride.test.ts | 296 ++++++++++++++++++ 9 files changed, 527 insertions(+), 192 deletions(-) create mode 100644 packages/evolution/test/TxBuilder.ProtocolParamsOverride.test.ts diff --git a/packages/evolution/src/sdk/builders/TransactionBuilder.ts b/packages/evolution/src/sdk/builders/TransactionBuilder.ts index 2e22c01b..d6d05d1b 100644 --- a/packages/evolution/src/sdk/builders/TransactionBuilder.ts +++ b/packages/evolution/src/sdk/builders/TransactionBuilder.ts @@ -468,12 +468,32 @@ export interface TxBuilderState { */ export interface BuildOptions { /** - * Override protocol parameters for this specific transaction build. + * Override protocol parameters for fee calculation. + * + * @deprecated Use `fullProtocolParameters` instead — it covers all fee-calc fields + * (`minFeeA`/`minFeeB` → `minFeeCoefficient`/`minFeeConstant`, `coinsPerUtxoByte`, + * `maxTxSize`, `priceMem`, `priceStep`, `minFeeRefScriptCostPerByte`) and is derived + * automatically when `fullProtocolParameters` is present. * * @since 2.0.0 */ readonly protocolParameters?: ProtocolParameters + /** + * Full protocol parameters override for all transaction build operations. + * + * When provided, ALL internal phases and operations will use these parameters + * instead of calling the provider's `getProtocolParameters` API. This prevents + * any network round-trips for protocol parameter fetching during the build. + * + * Includes all fields required for: script evaluation (cost models), stake/pool/DRep/ + * governance action deposits, and script data hash computation. Fee-calc fields + * (`protocolParameters`) are also derived from this automatically. + * + * @since 2.0.0 + */ + readonly fullProtocolParameters?: Provider.ProtocolParameters + /** * Coin selection strategy for automatic input selection. * @@ -669,7 +689,7 @@ export class BuildOptionsTag extends Context.Tag("BuildOptions") +export type ProgramStep = Effect.Effect // ============================================================================ // Voter Key @@ -1651,9 +1671,7 @@ export type TransactionBuilder = SigningTransactionBuilder | ReadOnlyTransaction export function makeTxBuilder( config: TxBuilderConfig & { wallet: Wallet.SigningWallet | Wallet.ApiWallet } ): SigningTransactionBuilder -export function makeTxBuilder( - config: TxBuilderConfig & { wallet: Wallet.ReadOnlyWallet } -): ReadOnlyTransactionBuilder +export function makeTxBuilder(config: TxBuilderConfig & { wallet: Wallet.ReadOnlyWallet }): ReadOnlyTransactionBuilder export function makeTxBuilder(config: TxBuilderConfig & { wallet?: undefined }): ReadOnlyTransactionBuilder export function makeTxBuilder(config: TxBuilderConfig): SigningTransactionBuilder | ReadOnlyTransactionBuilder { return BuilderFactory.makeTxBuilder(config) diff --git a/packages/evolution/src/sdk/builders/internal/resolve.ts b/packages/evolution/src/sdk/builders/internal/resolve.ts index a6585496..d7d1e652 100644 --- a/packages/evolution/src/sdk/builders/internal/resolve.ts +++ b/packages/evolution/src/sdk/builders/internal/resolve.ts @@ -23,6 +23,19 @@ export const resolveProtocolParameters = ( return Effect.succeed(options.protocolParameters) } + if (options?.fullProtocolParameters !== undefined) { + const p = options.fullProtocolParameters + return Effect.succeed({ + minFeeCoefficient: BigInt(p.minFeeA), + minFeeConstant: BigInt(p.minFeeB), + coinsPerUtxoByte: p.coinsPerUtxoByte, + maxTxSize: p.maxTxSize, + priceMem: p.priceMem, + priceStep: p.priceStep, + minFeeRefScriptCostPerByte: p.minFeeRefScriptCostPerByte + }) + } + const provider = config.provider if (provider !== undefined) { return Effect.map( diff --git a/packages/evolution/src/sdk/builders/internal/txBuilder.ts b/packages/evolution/src/sdk/builders/internal/txBuilder.ts index 0c2bb87a..f7e410fd 100644 --- a/packages/evolution/src/sdk/builders/internal/txBuilder.ts +++ b/packages/evolution/src/sdk/builders/internal/txBuilder.ts @@ -35,7 +35,13 @@ import * as CoreUTxO from "../../../UTxO.js" import * as VKey from "../../../VKey.js" import * as Withdrawals from "../../../Withdrawals.js" import type { UnfrackOptions } from "../TransactionBuilder.js" -import { BuildOptionsTag, TransactionBuilderError, TxBuilderConfigTag, TxContext, voterToKey } from "../TransactionBuilder.js" +import { + BuildOptionsTag, + TransactionBuilderError, + TxBuilderConfigTag, + TxContext, + voterToKey +} from "../TransactionBuilder.js" import * as Unfrack from "../Unfrack.js" // ============================================================================ @@ -526,29 +532,28 @@ export const assembleTransaction = ( let scriptDataHash: ReturnType | undefined let redeemersConcrete: Redeemers.RedeemerMap | undefined if (redeemers.length > 0) { - // Get config to access provider for full protocol parameters const config = yield* TxBuilderConfigTag - - if (!config.provider) { - return yield* Effect.fail( - new TransactionBuilderError({ - message: - "Script transactions require a provider to fetch full protocol parameters for scriptDataHash calculation", - cause: { redeemerCount: redeemers.length } - }) - ) - } - - // Fetch full protocol params from provider (includes cost models) - const fullProtocolParams = yield* config.provider.effect.getProtocolParameters().pipe( - Effect.mapError( - (providerError) => - new TransactionBuilderError({ - message: `Failed to fetch full protocol parameters for scriptDataHash calculation: ${providerError.message}`, - cause: providerError - }) - ) - ) + const buildOptions = yield* BuildOptionsTag + + const fullProtocolParams = yield* buildOptions.fullProtocolParameters + ? Effect.succeed(buildOptions.fullProtocolParameters) + : !config.provider + ? Effect.fail( + new TransactionBuilderError({ + message: + "Script transactions require a provider to fetch full protocol parameters for scriptDataHash calculation", + cause: { redeemerCount: redeemers.length } + }) + ) + : config.provider.effect.getProtocolParameters().pipe( + Effect.mapError( + (providerError) => + new TransactionBuilderError({ + message: `Failed to fetch full protocol parameters for scriptDataHash calculation: ${providerError.message}`, + cause: providerError + }) + ) + ) // Only include cost models for Plutus versions actually used in the transaction // The scriptDataHash must use the same languages as the node will compute @@ -739,9 +744,8 @@ export const assembleTransaction = ( * @since 2.0.0 * @category fee-calculation */ -export const calculateTransactionSize = ( - transaction: Transaction.Transaction -): number => Transaction.toCBORBytes(transaction).length +export const calculateTransactionSize = (transaction: Transaction.Transaction): number => + Transaction.toCBORBytes(transaction).length /** * Calculate minimum transaction fee based on protocol parameters. diff --git a/packages/evolution/src/sdk/builders/operations/Governance.ts b/packages/evolution/src/sdk/builders/operations/Governance.ts index 0c6e2bb1..6b136db1 100644 --- a/packages/evolution/src/sdk/builders/operations/Governance.ts +++ b/packages/evolution/src/sdk/builders/operations/Governance.ts @@ -10,7 +10,7 @@ import { Effect, Ref } from "effect" import * as Bytes from "../../../Bytes.js" import * as Certificate from "../../../Certificate.js" import * as RedeemerBuilder from "../RedeemerBuilder.js" -import { TransactionBuilderError, TxBuilderConfigTag, TxContext } from "../TransactionBuilder.js" +import { TransactionBuilderError, BuildOptionsTag, TxBuilderConfigTag, TxContext } from "../TransactionBuilder.js" import type { AuthCommitteeHotParams, DeregisterDRepParams, @@ -33,16 +33,14 @@ import type { */ export const createRegisterDRepProgram = ( params: RegisterDRepParams -): Effect.Effect => +): Effect.Effect => Effect.gen(function* () { const ctx = yield* TxContext const config = yield* TxBuilderConfigTag + const buildOptions = yield* BuildOptionsTag - // Check if script-controlled const isScriptControlled = params.drepCredential._tag === "ScriptHash" - // Script-controlled DRep registration requires a redeemer (Publishing purpose). - // The script is invoked to authorize the registration. if (isScriptControlled && !params.redeemer) { return yield* Effect.fail( new TransactionBuilderError({ @@ -51,23 +49,22 @@ export const createRegisterDRepProgram = ( ) } - // Get drepDeposit from protocol parameters via provider - if (!config.provider) { - return yield* Effect.fail( - new TransactionBuilderError({ - message: "Provider required to fetch drepDeposit for DRep registration" - }) - ) - } - - const protocolParams = yield* config.provider.effect.getProtocolParameters().pipe( - Effect.mapError( - (err) => - new TransactionBuilderError({ - message: `Failed to fetch protocol parameters: ${err.message}` - }) - ) - ) + const protocolParams = yield* buildOptions.fullProtocolParameters + ? Effect.succeed(buildOptions.fullProtocolParameters) + : !config.provider + ? Effect.fail( + new TransactionBuilderError({ + message: "Provider required to fetch drepDeposit for DRep registration" + }) + ) + : config.provider.effect.getProtocolParameters().pipe( + Effect.mapError( + (err) => + new TransactionBuilderError({ + message: `Failed to fetch protocol parameters: ${err.message}` + }) + ) + ) const drepDeposit = protocolParams.drepDeposit // Create RegDrepCert certificate with deposit @@ -196,21 +193,12 @@ export const createUpdateDRepProgram = ( */ export const createDeregisterDRepProgram = ( params: DeregisterDRepParams -): Effect.Effect => +): Effect.Effect => Effect.gen(function* () { const ctx = yield* TxContext const config = yield* TxBuilderConfigTag + const buildOptions = yield* BuildOptionsTag - // Get drepDeposit from protocol parameters via provider - if (!config.provider) { - return yield* Effect.fail( - new TransactionBuilderError({ - message: "Provider required to fetch drepDeposit for DRep deregistration" - }) - ) - } - - // Check if script-controlled const isScriptControlled = params.drepCredential._tag === "ScriptHash" if (isScriptControlled && !params.redeemer) { @@ -221,14 +209,22 @@ export const createDeregisterDRepProgram = ( ) } - const protocolParams = yield* config.provider.effect.getProtocolParameters().pipe( - Effect.mapError( - (err) => - new TransactionBuilderError({ - message: `Failed to fetch protocol parameters: ${err.message}` - }) - ) - ) + const protocolParams = yield* buildOptions.fullProtocolParameters + ? Effect.succeed(buildOptions.fullProtocolParameters) + : !config.provider + ? Effect.fail( + new TransactionBuilderError({ + message: "Provider required to fetch drepDeposit for DRep deregistration" + }) + ) + : config.provider.effect.getProtocolParameters().pipe( + Effect.mapError( + (err) => + new TransactionBuilderError({ + message: `Failed to fetch protocol parameters: ${err.message}` + }) + ) + ) const drepDeposit = protocolParams.drepDeposit // Create UnregDrepCert certificate with deposit refund diff --git a/packages/evolution/src/sdk/builders/operations/Pool.ts b/packages/evolution/src/sdk/builders/operations/Pool.ts index 3b1855ec..905d3ba5 100644 --- a/packages/evolution/src/sdk/builders/operations/Pool.ts +++ b/packages/evolution/src/sdk/builders/operations/Pool.ts @@ -9,7 +9,7 @@ import { Effect, Ref } from "effect" import * as Certificate from "../../../Certificate.js" import * as PoolKeyHash from "../../../PoolKeyHash.js" -import { TransactionBuilderError, TxBuilderConfigTag, TxContext } from "../TransactionBuilder.js" +import { TransactionBuilderError, BuildOptionsTag, TxBuilderConfigTag, TxContext } from "../TransactionBuilder.js" import type { RegisterPoolParams, RetirePoolParams } from "./Operations.js" // ============================================================================ @@ -26,30 +26,28 @@ import type { RegisterPoolParams, RetirePoolParams } from "./Operations.js" */ export const createRegisterPoolProgram = ( params: RegisterPoolParams -): Effect.Effect => +): Effect.Effect => Effect.gen(function* () { const ctx = yield* TxContext const config = yield* TxBuilderConfigTag + const buildOptions = yield* BuildOptionsTag - // TODO: protocol param should be resolved earlier in builder phases, not here - // protocol param can come from the provider or the build options directly - // Get poolDeposit from protocol parameters via provider - if (!config.provider) { - return yield* Effect.fail( - new TransactionBuilderError({ - message: "Provider required to fetch poolDeposit for pool registration" - }) - ) - } - - const protocolParams = yield* config.provider.effect.getProtocolParameters().pipe( - Effect.mapError( - (err) => - new TransactionBuilderError({ - message: `Failed to fetch protocol parameters: ${err.message}` - }) - ) - ) + const protocolParams = yield* buildOptions.fullProtocolParameters + ? Effect.succeed(buildOptions.fullProtocolParameters) + : !config.provider + ? Effect.fail( + new TransactionBuilderError({ + message: "Provider required to fetch poolDeposit for pool registration" + }) + ) + : config.provider.effect.getProtocolParameters().pipe( + Effect.mapError( + (err) => + new TransactionBuilderError({ + message: `Failed to fetch protocol parameters: ${err.message}` + }) + ) + ) const poolDeposit = protocolParams.poolDeposit // Create PoolRegistration certificate diff --git a/packages/evolution/src/sdk/builders/operations/Propose.ts b/packages/evolution/src/sdk/builders/operations/Propose.ts index 89f80d3b..37c3ffb3 100644 --- a/packages/evolution/src/sdk/builders/operations/Propose.ts +++ b/packages/evolution/src/sdk/builders/operations/Propose.ts @@ -9,7 +9,7 @@ import { Effect, Ref } from "effect" import * as ProposalProcedure from "../../../ProposalProcedure.js" import * as ProposalProcedures from "../../../ProposalProcedures.js" -import { TransactionBuilderError, TxBuilderConfigTag, TxContext } from "../TransactionBuilder.js" +import { TransactionBuilderError, BuildOptionsTag, TxBuilderConfigTag, TxContext } from "../TransactionBuilder.js" import type { ProposeParams } from "./Operations.js" /** @@ -29,28 +29,28 @@ import type { ProposeParams } from "./Operations.js" */ export const createProposeProgram = ( params: ProposeParams -): Effect.Effect => +): Effect.Effect => Effect.gen(function* () { const ctx = yield* TxContext const config = yield* TxBuilderConfigTag + const buildOptions = yield* BuildOptionsTag - // 1. Get govActionDeposit from protocol parameters via provider - if (!config.provider) { - return yield* Effect.fail( - new TransactionBuilderError({ - message: "Provider required to fetch govActionDeposit for governance proposal" - }) - ) - } - - const protocolParams = yield* config.provider.effect.getProtocolParameters().pipe( - Effect.mapError( - (err) => - new TransactionBuilderError({ - message: `Failed to fetch protocol parameters: ${err.message}` - }) - ) - ) + const protocolParams = yield* buildOptions.fullProtocolParameters + ? Effect.succeed(buildOptions.fullProtocolParameters) + : !config.provider + ? Effect.fail( + new TransactionBuilderError({ + message: "Provider required to fetch govActionDeposit for governance proposal" + }) + ) + : config.provider.effect.getProtocolParameters().pipe( + Effect.mapError( + (err) => + new TransactionBuilderError({ + message: `Failed to fetch protocol parameters: ${err.message}` + }) + ) + ) const govActionDeposit = protocolParams.govActionDeposit // 2. Construct ProposalProcedure with fetched deposit diff --git a/packages/evolution/src/sdk/builders/operations/Stake.ts b/packages/evolution/src/sdk/builders/operations/Stake.ts index 7ae1e2e3..7bab333a 100644 --- a/packages/evolution/src/sdk/builders/operations/Stake.ts +++ b/packages/evolution/src/sdk/builders/operations/Stake.ts @@ -11,7 +11,7 @@ import * as Bytes from "../../../Bytes.js" import * as Certificate from "../../../Certificate.js" import * as RewardAccount from "../../../RewardAccount.js" import * as RedeemerBuilder from "../RedeemerBuilder.js" -import { TransactionBuilderError, TxBuilderConfigTag, TxContext } from "../TransactionBuilder.js" +import { TransactionBuilderError, BuildOptionsTag, TxBuilderConfigTag, TxContext } from "../TransactionBuilder.js" import type { DelegateToDRepParams, DelegateToParams, @@ -33,21 +33,12 @@ import type { */ export const createRegisterStakeProgram = ( params: RegisterStakeParams -): Effect.Effect => +): Effect.Effect => Effect.gen(function* () { const ctx = yield* TxContext const config = yield* TxBuilderConfigTag + const buildOptions = yield* BuildOptionsTag - // Get keyDeposit from protocol parameters via provider - if (!config.provider) { - return yield* Effect.fail( - new TransactionBuilderError({ - message: "Provider required to fetch keyDeposit for stake registration" - }) - ) - } - - // Check if script-controlled const isScriptControlled = params.stakeCredential._tag === "ScriptHash" if (isScriptControlled && !params.redeemer) { @@ -58,14 +49,22 @@ export const createRegisterStakeProgram = ( ) } - const protocolParams = yield* config.provider.effect.getProtocolParameters().pipe( - Effect.mapError( - (err) => - new TransactionBuilderError({ - message: `Failed to fetch protocol parameters: ${err.message}` - }) - ) - ) + const protocolParams = yield* buildOptions.fullProtocolParameters + ? Effect.succeed(buildOptions.fullProtocolParameters) + : !config.provider + ? Effect.fail( + new TransactionBuilderError({ + message: "Provider required to fetch keyDeposit for stake registration" + }) + ) + : config.provider.effect.getProtocolParameters().pipe( + Effect.mapError( + (err) => + new TransactionBuilderError({ + message: `Failed to fetch protocol parameters: ${err.message}` + }) + ) + ) const keyDeposit = protocolParams.keyDeposit // Create RegCert (Conway-era) certificate with deposit @@ -387,12 +386,12 @@ export const createDelegateToPoolAndDRepProgram = ( */ export const createRegisterAndDelegateToProgram = ( params: RegisterAndDelegateToParams -): Effect.Effect => +): Effect.Effect => Effect.gen(function* () { const ctx = yield* TxContext const config = yield* TxBuilderConfigTag + const buildOptions = yield* BuildOptionsTag - // Validate at least one delegation target if (!params.poolKeyHash && !params.drep) { return yield* Effect.fail( new TransactionBuilderError({ @@ -401,23 +400,22 @@ export const createRegisterAndDelegateToProgram = ( ) } - // Get keyDeposit from protocol parameters via provider - if (!config.provider) { - return yield* Effect.fail( - new TransactionBuilderError({ - message: "Provider required to fetch keyDeposit for stake registration" - }) - ) - } - - const protocolParams = yield* config.provider.effect.getProtocolParameters().pipe( - Effect.mapError( - (err) => - new TransactionBuilderError({ - message: `Failed to fetch protocol parameters: ${err.message}` - }) - ) - ) + const protocolParams = yield* buildOptions.fullProtocolParameters + ? Effect.succeed(buildOptions.fullProtocolParameters) + : !config.provider + ? Effect.fail( + new TransactionBuilderError({ + message: "Provider required to fetch keyDeposit for stake registration" + }) + ) + : config.provider.effect.getProtocolParameters().pipe( + Effect.mapError( + (err) => + new TransactionBuilderError({ + message: `Failed to fetch protocol parameters: ${err.message}` + }) + ) + ) const keyDeposit = protocolParams.keyDeposit // Check if script-controlled @@ -516,12 +514,12 @@ export const createRegisterAndDelegateToProgram = ( */ export const createDeregisterStakeProgram = ( params: DeregisterStakeParams -): Effect.Effect => +): Effect.Effect => Effect.gen(function* () { const ctx = yield* TxContext const config = yield* TxBuilderConfigTag + const buildOptions = yield* BuildOptionsTag - // Check if script-controlled const isScriptControlled = params.stakeCredential._tag === "ScriptHash" if (isScriptControlled && !params.redeemer) { @@ -532,23 +530,22 @@ export const createDeregisterStakeProgram = ( ) } - // Get keyDeposit from protocol parameters via provider - if (!config.provider) { - return yield* Effect.fail( - new TransactionBuilderError({ - message: "Provider required to fetch keyDeposit for stake deregistration" - }) - ) - } - - const protocolParams = yield* config.provider.effect.getProtocolParameters().pipe( - Effect.mapError( - (err) => - new TransactionBuilderError({ - message: `Failed to fetch protocol parameters: ${err.message}` - }) - ) - ) + const protocolParams = yield* buildOptions.fullProtocolParameters + ? Effect.succeed(buildOptions.fullProtocolParameters) + : !config.provider + ? Effect.fail( + new TransactionBuilderError({ + message: "Provider required to fetch keyDeposit for stake deregistration" + }) + ) + : config.provider.effect.getProtocolParameters().pipe( + Effect.mapError( + (err) => + new TransactionBuilderError({ + message: `Failed to fetch protocol parameters: ${err.message}` + }) + ) + ) const keyDeposit = protocolParams.keyDeposit // Create UnregCert (Conway-era) certificate with deposit refund diff --git a/packages/evolution/src/sdk/builders/phases/Evaluation.ts b/packages/evolution/src/sdk/builders/phases/Evaluation.ts index 2bb41dde..6bc917ba 100644 --- a/packages/evolution/src/sdk/builders/phases/Evaluation.ts +++ b/packages/evolution/src/sdk/builders/phases/Evaluation.ts @@ -19,8 +19,22 @@ import type * as Provider from "../../provider/Provider.js" import * as EvaluationStateManager from "../EvaluationStateManager.js" import { assembleTransaction } from "../internal/txBuilder.js" import type { IndexedInput } from "../RedeemerBuilder.js" -import type { DeferredRedeemerData, EvaluationContext, PhaseResult, RedeemerData, ScriptFailure } from "../TransactionBuilder.js" -import { BuildOptionsTag, EvaluationError, PhaseContextTag, TransactionBuilderError, TxBuilderConfigTag, TxContext, voterToKey } from "../TransactionBuilder.js" +import type { + DeferredRedeemerData, + EvaluationContext, + PhaseResult, + RedeemerData, + ScriptFailure +} from "../TransactionBuilder.js" +import { + BuildOptionsTag, + EvaluationError, + PhaseContextTag, + TransactionBuilderError, + TxBuilderConfigTag, + TxContext, + voterToKey +} from "../TransactionBuilder.js" /** * Convert ProtocolParameters cost models to CostModels core type for evaluation. @@ -277,26 +291,25 @@ export const executeEvaluation = (): Effect.Effect< ) } - // Step 2.5: Fetch full protocol parameters (needed for cost models and execution limits) - if (!config.provider) { - return yield* Effect.fail( - new TransactionBuilderError({ - message: - "Script evaluation requires a provider to fetch full protocol parameters (cost models, execution limits)", - cause: { redeemerCount: state.redeemers.size } - }) - ) - } - - const fullProtocolParams = yield* config.provider.effect.getProtocolParameters().pipe( - Effect.mapError( - (providerError) => - new TransactionBuilderError({ - message: `Failed to fetch full protocol parameters for evaluation: ${providerError.message}`, - cause: providerError - }) - ) - ) + const fullProtocolParams = yield* buildOptions.fullProtocolParameters + ? Effect.succeed(buildOptions.fullProtocolParameters) + : !config.provider + ? Effect.fail( + new TransactionBuilderError({ + message: + "Script evaluation requires a provider to fetch full protocol parameters (cost models, execution limits)", + cause: { redeemerCount: state.redeemers.size } + }) + ) + : config.provider.effect.getProtocolParameters().pipe( + Effect.mapError( + (providerError) => + new TransactionBuilderError({ + message: `Failed to fetch full protocol parameters for evaluation: ${providerError.message}`, + cause: providerError + }) + ) + ) // Step 3: Check if there are redeemers to evaluate (resolved or deferred) const hasResolvedRedeemers = state.redeemers.size > 0 diff --git a/packages/evolution/test/TxBuilder.ProtocolParamsOverride.test.ts b/packages/evolution/test/TxBuilder.ProtocolParamsOverride.test.ts new file mode 100644 index 00000000..dcc751a8 --- /dev/null +++ b/packages/evolution/test/TxBuilder.ProtocolParamsOverride.test.ts @@ -0,0 +1,296 @@ +import { describe, expect, it } from "@effect/vitest" +import { Effect } from "effect" + +import * as Address from "../src/Address.js" +import * as Credential from "../src/Credential.js" +import type { Provider as ProviderType } from "../src/sdk/provider/Provider.js" +import { makeTxBuilder } from "../src/sdk/builders/TransactionBuilder.js" +import { mainnet } from "../src/sdk/client/index.js" +import * as CoreUTxO from "../src/UTxO.js" +import { createCoreTestUtxo } from "./utils/utxo-helpers.js" + +const CHANGE_ADDRESS = + "addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3n0d3vllmyqwsx5wktcd8cc3sq835lu7drv2xwl2wywfgs68faae" + +const FULL_PROTOCOL_PARAMS = { + minFeeA: 44, + minFeeB: 155_381, + maxTxSize: 16_384, + maxValSize: 5_000, + keyDeposit: 2_000_000n, + poolDeposit: 500_000_000n, + drepDeposit: 500_000_000n, + govActionDeposit: 100_000_000_000n, + priceMem: 0.0577, + priceStep: 0.0000721, + maxTxExMem: 14_000_000n, + maxTxExSteps: 10_000_000_000n, + coinsPerUtxoByte: 4_310n, + collateralPercentage: 150, + maxCollateralInputs: 3, + minFeeRefScriptCostPerByte: 15, + costModels: { + PlutusV1: {} as Record, + PlutusV2: {} as Record, + PlutusV3: {} as Record + } +} satisfies ProviderType["effect"] extends { getProtocolParameters: () => Effect.Effect } ? P : never + +const PROTOCOL_PARAMS_FOR_FEE = { + minFeeCoefficient: 44n, + minFeeConstant: 155_381n, + coinsPerUtxoByte: 4_310n, + maxTxSize: 16_384 +} + +const makeStakeCredential = () => Credential.makeKeyHash(new Uint8Array(28).fill(0xab)) +const makeDRepCredential = () => Credential.makeKeyHash(new Uint8Array(28).fill(0xcd)) + +const makeFundedUtxos = (lovelace: bigint): Array => [ + createCoreTestUtxo({ + transactionId: "a".repeat(64), + index: 0n, + address: CHANGE_ADDRESS, + lovelace + }) +] + +const makeSpyProvider = () => { + let callCount = 0 + + const notImpl = (name: string) => () => { + throw new Error(`SpyProvider.${name}: not implemented`) + } + + const effect = { + getProtocolParameters: () => { + callCount++ + return Effect.succeed(FULL_PROTOCOL_PARAMS) + }, + getUtxos: notImpl("getUtxos"), + getUtxosWithUnit: notImpl("getUtxosWithUnit"), + getUtxoByUnit: notImpl("getUtxoByUnit"), + getUtxosByOutRef: notImpl("getUtxosByOutRef"), + getDelegation: notImpl("getDelegation"), + getDatum: notImpl("getDatum"), + awaitTx: notImpl("awaitTx"), + submitTx: notImpl("submitTx"), + evaluateTx: notImpl("evaluateTx") + } + + const provider = { effect } as unknown as ProviderType + + return { provider, getCallCount: () => callCount } +} + +const baseConfig = { chain: mainnet } + +describe("fullProtocolParameters override — registerStake", () => { + it("succeeds without a provider when fullProtocolParameters is supplied", async () => { + const utxos = makeFundedUtxos(5_000_000n) // covers keyDeposit(2M) + fee + + await expect( + makeTxBuilder(baseConfig) + .registerStake({ stakeCredential: makeStakeCredential() }) + .build({ + changeAddress: Address.fromBech32(CHANGE_ADDRESS), + availableUtxos: utxos, + fullProtocolParameters: FULL_PROTOCOL_PARAMS + }) + ).resolves.toBeDefined() + }) + + it("does not call provider.getProtocolParameters when fullProtocolParameters is supplied", async () => { + const spy = makeSpyProvider() + const utxos = makeFundedUtxos(5_000_000n) + + await makeTxBuilder({ chain: mainnet, provider: spy.provider }) + .registerStake({ stakeCredential: makeStakeCredential() }) + .build({ + changeAddress: Address.fromBech32(CHANGE_ADDRESS), + availableUtxos: utxos, + fullProtocolParameters: FULL_PROTOCOL_PARAMS + }) + + expect(spy.getCallCount()).toBe(0) + }) + + it("calls provider.getProtocolParameters when fullProtocolParameters is absent", async () => { + const spy = makeSpyProvider() + const utxos = makeFundedUtxos(5_000_000n) + + await makeTxBuilder({ chain: mainnet, provider: spy.provider }) + .registerStake({ stakeCredential: makeStakeCredential() }) + .build({ + changeAddress: Address.fromBech32(CHANGE_ADDRESS), + availableUtxos: utxos + // fullProtocolParameters deliberately omitted + }) + + expect(spy.getCallCount()).toBeGreaterThan(0) + }) + + it("fails with a descriptive error when fullProtocolParameters absent and no provider", async () => { + const utxos = makeFundedUtxos(5_000_000n) + + await expect( + makeTxBuilder(baseConfig) + .registerStake({ stakeCredential: makeStakeCredential() }) + .build({ + changeAddress: Address.fromBech32(CHANGE_ADDRESS), + availableUtxos: utxos, + protocolParameters: PROTOCOL_PARAMS_FOR_FEE + }) + ).rejects.toThrow(/Provider required to fetch keyDeposit for stake registration/) + }) +}) + +describe("fullProtocolParameters override — deregisterStake", () => { + it("succeeds without a provider when fullProtocolParameters is supplied", async () => { + const utxos = makeFundedUtxos(2_000_000n) // only fee needed; deposit is refunded + + await expect( + makeTxBuilder(baseConfig) + .deregisterStake({ stakeCredential: makeStakeCredential() }) + .build({ + changeAddress: Address.fromBech32(CHANGE_ADDRESS), + availableUtxos: utxos, + fullProtocolParameters: FULL_PROTOCOL_PARAMS + }) + ).resolves.toBeDefined() + }) + + it("does not call provider.getProtocolParameters when fullProtocolParameters is supplied", async () => { + const spy = makeSpyProvider() + const utxos = makeFundedUtxos(2_000_000n) + + await makeTxBuilder({ chain: mainnet, provider: spy.provider }) + .deregisterStake({ stakeCredential: makeStakeCredential() }) + .build({ + changeAddress: Address.fromBech32(CHANGE_ADDRESS), + availableUtxos: utxos, + fullProtocolParameters: FULL_PROTOCOL_PARAMS + }) + + expect(spy.getCallCount()).toBe(0) + }) + + it("fails with a descriptive error when fullProtocolParameters absent and no provider", async () => { + const utxos = makeFundedUtxos(2_000_000n) + + await expect( + makeTxBuilder(baseConfig) + .deregisterStake({ stakeCredential: makeStakeCredential() }) + .build({ + changeAddress: Address.fromBech32(CHANGE_ADDRESS), + availableUtxos: utxos, + protocolParameters: PROTOCOL_PARAMS_FOR_FEE + }) + ).rejects.toThrow(/Provider required to fetch keyDeposit for stake deregistration/) + }) +}) + +describe("fullProtocolParameters override — registerDRep", () => { + it("succeeds without a provider when fullProtocolParameters is supplied", async () => { + const utxos = makeFundedUtxos(503_000_000n) // drepDeposit(500M) + fee + + await expect( + makeTxBuilder(baseConfig) + .registerDRep({ drepCredential: makeDRepCredential() }) + .build({ + changeAddress: Address.fromBech32(CHANGE_ADDRESS), + availableUtxos: utxos, + fullProtocolParameters: FULL_PROTOCOL_PARAMS + }) + ).resolves.toBeDefined() + }) + + it("does not call provider.getProtocolParameters when fullProtocolParameters is supplied", async () => { + const spy = makeSpyProvider() + const utxos = makeFundedUtxos(503_000_000n) + + await makeTxBuilder({ chain: mainnet, provider: spy.provider }) + .registerDRep({ drepCredential: makeDRepCredential() }) + .build({ + changeAddress: Address.fromBech32(CHANGE_ADDRESS), + availableUtxos: utxos, + fullProtocolParameters: FULL_PROTOCOL_PARAMS + }) + + expect(spy.getCallCount()).toBe(0) + }) + + it("calls provider.getProtocolParameters when fullProtocolParameters is absent", async () => { + const spy = makeSpyProvider() + const utxos = makeFundedUtxos(503_000_000n) + + await makeTxBuilder({ chain: mainnet, provider: spy.provider }) + .registerDRep({ drepCredential: makeDRepCredential() }) + .build({ + changeAddress: Address.fromBech32(CHANGE_ADDRESS), + availableUtxos: utxos + // fullProtocolParameters deliberately omitted + }) + + expect(spy.getCallCount()).toBeGreaterThan(0) + }) + + it("fails with a descriptive error when fullProtocolParameters absent and no provider", async () => { + const utxos = makeFundedUtxos(503_000_000n) + + await expect( + makeTxBuilder(baseConfig) + .registerDRep({ drepCredential: makeDRepCredential() }) + .build({ + changeAddress: Address.fromBech32(CHANGE_ADDRESS), + availableUtxos: utxos, + protocolParameters: PROTOCOL_PARAMS_FOR_FEE + }) + ).rejects.toThrow(/Provider required to fetch drepDeposit for DRep registration/) + }) +}) + +describe("fullProtocolParameters override — deregisterDRep", () => { + it("succeeds without a provider when fullProtocolParameters is supplied", async () => { + const utxos = makeFundedUtxos(2_000_000n) // only fee needed; deposit is refunded + + await expect( + makeTxBuilder(baseConfig) + .deregisterDRep({ drepCredential: makeDRepCredential() }) + .build({ + changeAddress: Address.fromBech32(CHANGE_ADDRESS), + availableUtxos: utxos, + fullProtocolParameters: FULL_PROTOCOL_PARAMS + }) + ).resolves.toBeDefined() + }) + + it("does not call provider.getProtocolParameters when fullProtocolParameters is supplied", async () => { + const spy = makeSpyProvider() + const utxos = makeFundedUtxos(2_000_000n) + + await makeTxBuilder({ chain: mainnet, provider: spy.provider }) + .deregisterDRep({ drepCredential: makeDRepCredential() }) + .build({ + changeAddress: Address.fromBech32(CHANGE_ADDRESS), + availableUtxos: utxos, + fullProtocolParameters: FULL_PROTOCOL_PARAMS + }) + + expect(spy.getCallCount()).toBe(0) + }) + + it("fails with a descriptive error when fullProtocolParameters absent and no provider", async () => { + const utxos = makeFundedUtxos(2_000_000n) + + await expect( + makeTxBuilder(baseConfig) + .deregisterDRep({ drepCredential: makeDRepCredential() }) + .build({ + changeAddress: Address.fromBech32(CHANGE_ADDRESS), + availableUtxos: utxos, + protocolParameters: PROTOCOL_PARAMS_FOR_FEE + }) + ).rejects.toThrow(/Provider required to fetch drepDeposit for DRep deregistration/) + }) +}) From c85c6c15e07276c5ead6cd35981a7b3bb18bbedd Mon Sep 17 00:00:00 2001 From: Mavis2103 Date: Sun, 26 Apr 2026 14:09:22 +0700 Subject: [PATCH 2/4] refactor: reorder imports for consistency across governance, pool, propose, and stake modules --- packages/evolution/src/sdk/builders/operations/Governance.ts | 2 +- packages/evolution/src/sdk/builders/operations/Pool.ts | 2 +- packages/evolution/src/sdk/builders/operations/Propose.ts | 2 +- packages/evolution/src/sdk/builders/operations/Stake.ts | 2 +- .../evolution/test/TxBuilder.ProtocolParamsOverride.test.ts | 4 ++-- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/evolution/src/sdk/builders/operations/Governance.ts b/packages/evolution/src/sdk/builders/operations/Governance.ts index 6b136db1..8430a383 100644 --- a/packages/evolution/src/sdk/builders/operations/Governance.ts +++ b/packages/evolution/src/sdk/builders/operations/Governance.ts @@ -10,7 +10,7 @@ import { Effect, Ref } from "effect" import * as Bytes from "../../../Bytes.js" import * as Certificate from "../../../Certificate.js" import * as RedeemerBuilder from "../RedeemerBuilder.js" -import { TransactionBuilderError, BuildOptionsTag, TxBuilderConfigTag, TxContext } from "../TransactionBuilder.js" +import { BuildOptionsTag, TransactionBuilderError, TxBuilderConfigTag, TxContext } from "../TransactionBuilder.js" import type { AuthCommitteeHotParams, DeregisterDRepParams, diff --git a/packages/evolution/src/sdk/builders/operations/Pool.ts b/packages/evolution/src/sdk/builders/operations/Pool.ts index 905d3ba5..d74dd9af 100644 --- a/packages/evolution/src/sdk/builders/operations/Pool.ts +++ b/packages/evolution/src/sdk/builders/operations/Pool.ts @@ -9,7 +9,7 @@ import { Effect, Ref } from "effect" import * as Certificate from "../../../Certificate.js" import * as PoolKeyHash from "../../../PoolKeyHash.js" -import { TransactionBuilderError, BuildOptionsTag, TxBuilderConfigTag, TxContext } from "../TransactionBuilder.js" +import { BuildOptionsTag, TransactionBuilderError, TxBuilderConfigTag, TxContext } from "../TransactionBuilder.js" import type { RegisterPoolParams, RetirePoolParams } from "./Operations.js" // ============================================================================ diff --git a/packages/evolution/src/sdk/builders/operations/Propose.ts b/packages/evolution/src/sdk/builders/operations/Propose.ts index 37c3ffb3..98237105 100644 --- a/packages/evolution/src/sdk/builders/operations/Propose.ts +++ b/packages/evolution/src/sdk/builders/operations/Propose.ts @@ -9,7 +9,7 @@ import { Effect, Ref } from "effect" import * as ProposalProcedure from "../../../ProposalProcedure.js" import * as ProposalProcedures from "../../../ProposalProcedures.js" -import { TransactionBuilderError, BuildOptionsTag, TxBuilderConfigTag, TxContext } from "../TransactionBuilder.js" +import { BuildOptionsTag, TransactionBuilderError, TxBuilderConfigTag, TxContext } from "../TransactionBuilder.js" import type { ProposeParams } from "./Operations.js" /** diff --git a/packages/evolution/src/sdk/builders/operations/Stake.ts b/packages/evolution/src/sdk/builders/operations/Stake.ts index 7bab333a..c50c47e3 100644 --- a/packages/evolution/src/sdk/builders/operations/Stake.ts +++ b/packages/evolution/src/sdk/builders/operations/Stake.ts @@ -11,7 +11,7 @@ import * as Bytes from "../../../Bytes.js" import * as Certificate from "../../../Certificate.js" import * as RewardAccount from "../../../RewardAccount.js" import * as RedeemerBuilder from "../RedeemerBuilder.js" -import { TransactionBuilderError, BuildOptionsTag, TxBuilderConfigTag, TxContext } from "../TransactionBuilder.js" +import { BuildOptionsTag, TransactionBuilderError, TxBuilderConfigTag, TxContext } from "../TransactionBuilder.js" import type { DelegateToDRepParams, DelegateToParams, diff --git a/packages/evolution/test/TxBuilder.ProtocolParamsOverride.test.ts b/packages/evolution/test/TxBuilder.ProtocolParamsOverride.test.ts index dcc751a8..e4f002f5 100644 --- a/packages/evolution/test/TxBuilder.ProtocolParamsOverride.test.ts +++ b/packages/evolution/test/TxBuilder.ProtocolParamsOverride.test.ts @@ -3,10 +3,10 @@ import { Effect } from "effect" import * as Address from "../src/Address.js" import * as Credential from "../src/Credential.js" -import type { Provider as ProviderType } from "../src/sdk/provider/Provider.js" import { makeTxBuilder } from "../src/sdk/builders/TransactionBuilder.js" import { mainnet } from "../src/sdk/client/index.js" -import * as CoreUTxO from "../src/UTxO.js" +import type { Provider as ProviderType } from "../src/sdk/provider/Provider.js" +import type * as CoreUTxO from "../src/UTxO.js" import { createCoreTestUtxo } from "./utils/utxo-helpers.js" const CHANGE_ADDRESS = From afaf73e539c6e6a76b40217cd46f61f7d1db9344 Mon Sep 17 00:00:00 2001 From: solidsnakedev Date: Tue, 28 Apr 2026 18:07:37 -0600 Subject: [PATCH 3/4] refactor(provider): resolve full protocol parameters once via layer tag Introduce FullProtocolParametersTag resolved once per build instead of each operation independently calling the provider. Fix priority so fullProtocolParameters takes precedence over deprecated protocolParameters. --- .../src/sdk/builders/TransactionBuilder.ts | 24 +++++- .../src/sdk/builders/internal/layers.ts | 3 +- .../src/sdk/builders/internal/resolve.ts | 39 ++++++++- .../src/sdk/builders/internal/txBuilder.ts | 43 ++++------ .../src/sdk/builders/operations/Governance.ts | 58 ++++--------- .../src/sdk/builders/operations/Pool.ts | 30 ++----- .../src/sdk/builders/operations/Propose.ts | 30 ++----- .../src/sdk/builders/operations/Stake.ts | 86 ++++++------------- .../src/sdk/builders/phases/Evaluation.ts | 50 ++++------- .../TxBuilder.ProtocolParamsOverride.test.ts | 14 +-- 10 files changed, 161 insertions(+), 216 deletions(-) diff --git a/packages/evolution/src/sdk/builders/TransactionBuilder.ts b/packages/evolution/src/sdk/builders/TransactionBuilder.ts index d6d05d1b..a1569c40 100644 --- a/packages/evolution/src/sdk/builders/TransactionBuilder.ts +++ b/packages/evolution/src/sdk/builders/TransactionBuilder.ts @@ -652,6 +652,20 @@ export class ProtocolParametersTag extends Context.Tag("ProtocolParameters")< ProtocolParameters >() {} +/** + * Context tag providing full protocol parameters (deposits, cost models, execution limits). + * + * Resolved once per build. Holds `undefined` when neither `fullProtocolParameters` + * nor a provider is available — operations that need it fail with a descriptive error. + * + * @since 2.0.0 + * @category context + */ +export class FullProtocolParametersTag extends Context.Tag("FullProtocolParameters")< + FullProtocolParametersTag, + Provider.ProtocolParameters | undefined +>() {} + /** * Context tag providing the builder configuration. * @@ -689,7 +703,11 @@ export class BuildOptionsTag extends Context.Tag("BuildOptions") +export type ProgramStep = Effect.Effect< + void, + TransactionBuilderError, + TxContext | TxBuilderConfigTag | BuildOptionsTag | FullProtocolParametersTag +> // ============================================================================ // Voter Key @@ -1671,7 +1689,9 @@ export type TransactionBuilder = SigningTransactionBuilder | ReadOnlyTransaction export function makeTxBuilder( config: TxBuilderConfig & { wallet: Wallet.SigningWallet | Wallet.ApiWallet } ): SigningTransactionBuilder -export function makeTxBuilder(config: TxBuilderConfig & { wallet: Wallet.ReadOnlyWallet }): ReadOnlyTransactionBuilder +export function makeTxBuilder( + config: TxBuilderConfig & { wallet: Wallet.ReadOnlyWallet } +): ReadOnlyTransactionBuilder export function makeTxBuilder(config: TxBuilderConfig & { wallet?: undefined }): ReadOnlyTransactionBuilder export function makeTxBuilder(config: TxBuilderConfig): SigningTransactionBuilder | ReadOnlyTransactionBuilder { return BuilderFactory.makeTxBuilder(config) diff --git a/packages/evolution/src/sdk/builders/internal/layers.ts b/packages/evolution/src/sdk/builders/internal/layers.ts index a94ab8ab..39df8ed6 100644 --- a/packages/evolution/src/sdk/builders/internal/layers.ts +++ b/packages/evolution/src/sdk/builders/internal/layers.ts @@ -1,7 +1,7 @@ import { Layer, Ref } from "effect" import type { BuildOptions, PhaseContext, TxBuilderConfig } from "../TransactionBuilder.js" -import { AvailableUtxosTag, BuildOptionsTag, ChangeAddressTag, PhaseContextTag, ProtocolParametersTag, TxBuilderConfigTag, TxContext } from "../TransactionBuilder.js" +import { AvailableUtxosTag, BuildOptionsTag, ChangeAddressTag, FullProtocolParametersTag, PhaseContextTag, ProtocolParametersTag, TxBuilderConfigTag, TxContext } from "../TransactionBuilder.js" import * as BuilderResolve from "./resolve.js" import * as BuilderState from "./state.js" @@ -30,6 +30,7 @@ export const makeBuildRuntimeLayer = ( Layer.succeed(TxBuilderConfigTag, config), Layer.succeed(BuildOptionsTag, buildOptions), Layer.effect(ProtocolParametersTag, BuilderResolve.resolveProtocolParameters(config, buildOptions)), + Layer.effect(FullProtocolParametersTag, BuilderResolve.resolveFullProtocolParameters(config, buildOptions)), Layer.effect(ChangeAddressTag, BuilderResolve.resolveChangeAddress(config, buildOptions)), Layer.effect(AvailableUtxosTag, BuilderResolve.resolveAvailableUtxos(config, buildOptions)) ) diff --git a/packages/evolution/src/sdk/builders/internal/resolve.ts b/packages/evolution/src/sdk/builders/internal/resolve.ts index d7d1e652..d3814cc2 100644 --- a/packages/evolution/src/sdk/builders/internal/resolve.ts +++ b/packages/evolution/src/sdk/builders/internal/resolve.ts @@ -19,10 +19,6 @@ export const resolveProtocolParameters = ( config: TxBuilderConfig, options?: BuildOptions ): Effect.Effect => { - if (options?.protocolParameters !== undefined) { - return Effect.succeed(options.protocolParameters) - } - if (options?.fullProtocolParameters !== undefined) { const p = options.fullProtocolParameters return Effect.succeed({ @@ -36,6 +32,10 @@ export const resolveProtocolParameters = ( }) } + if (options?.protocolParameters !== undefined) { + return Effect.succeed(options.protocolParameters) + } + const provider = config.provider if (provider !== undefined) { return Effect.map( @@ -60,6 +60,37 @@ export const resolveProtocolParameters = ( ) } +/** + * Resolve full protocol parameters once for the build layer. + * + * Returns `undefined` when neither `fullProtocolParameters` nor a provider is available. + * Operations that need it check for `undefined` and fail with a descriptive error. + * + * @since 2.0.0 + * @category builders + */ +export const resolveFullProtocolParameters = ( + config: TxBuilderConfig, + options?: BuildOptions +): Effect.Effect => { + if (options?.fullProtocolParameters) { + return Effect.succeed(options.fullProtocolParameters) + } + + if (config.provider) { + return config.provider.effect.getProtocolParameters().pipe( + Effect.mapError( + (err) => + new TransactionBuilderError({ + message: `Failed to fetch protocol parameters: ${err.message}` + }) + ) + ) + } + + return Effect.succeed(undefined) +} + /** * Resolve the build change address. * diff --git a/packages/evolution/src/sdk/builders/internal/txBuilder.ts b/packages/evolution/src/sdk/builders/internal/txBuilder.ts index f7e410fd..0f9ff847 100644 --- a/packages/evolution/src/sdk/builders/internal/txBuilder.ts +++ b/packages/evolution/src/sdk/builders/internal/txBuilder.ts @@ -34,14 +34,14 @@ import * as TxOut from "../../../TxOut.js" import * as CoreUTxO from "../../../UTxO.js" import * as VKey from "../../../VKey.js" import * as Withdrawals from "../../../Withdrawals.js" -import type { UnfrackOptions } from "../TransactionBuilder.js" import { BuildOptionsTag, + FullProtocolParametersTag, TransactionBuilderError, - TxBuilderConfigTag, + type TxBuilderConfigTag, TxContext, - voterToKey -} from "../TransactionBuilder.js" + type UnfrackOptions, + voterToKey} from "../TransactionBuilder.js" import * as Unfrack from "../Unfrack.js" // ============================================================================ @@ -282,7 +282,7 @@ export const assembleTransaction = ( inputs: ReadonlyArray, outputs: ReadonlyArray, fee: bigint -): Effect.Effect => +): Effect.Effect => Effect.gen(function* () { // Get state ref to access scripts and redeemers const stateRef = yield* TxContext @@ -532,28 +532,17 @@ export const assembleTransaction = ( let scriptDataHash: ReturnType | undefined let redeemersConcrete: Redeemers.RedeemerMap | undefined if (redeemers.length > 0) { - const config = yield* TxBuilderConfigTag - const buildOptions = yield* BuildOptionsTag - - const fullProtocolParams = yield* buildOptions.fullProtocolParameters - ? Effect.succeed(buildOptions.fullProtocolParameters) - : !config.provider - ? Effect.fail( - new TransactionBuilderError({ - message: - "Script transactions require a provider to fetch full protocol parameters for scriptDataHash calculation", - cause: { redeemerCount: redeemers.length } - }) - ) - : config.provider.effect.getProtocolParameters().pipe( - Effect.mapError( - (providerError) => - new TransactionBuilderError({ - message: `Failed to fetch full protocol parameters for scriptDataHash calculation: ${providerError.message}`, - cause: providerError - }) - ) - ) + const fullParamsOrUndefined = yield* FullProtocolParametersTag + + if (!fullParamsOrUndefined) { + return yield* Effect.fail( + new TransactionBuilderError({ + message: "Provider required to fetch protocol parameters for scriptDataHash calculation", + cause: { redeemerCount: redeemers.length } + }) + ) + } + const fullProtocolParams = fullParamsOrUndefined // Only include cost models for Plutus versions actually used in the transaction // The scriptDataHash must use the same languages as the node will compute diff --git a/packages/evolution/src/sdk/builders/operations/Governance.ts b/packages/evolution/src/sdk/builders/operations/Governance.ts index 8430a383..f3346a16 100644 --- a/packages/evolution/src/sdk/builders/operations/Governance.ts +++ b/packages/evolution/src/sdk/builders/operations/Governance.ts @@ -10,7 +10,7 @@ import { Effect, Ref } from "effect" import * as Bytes from "../../../Bytes.js" import * as Certificate from "../../../Certificate.js" import * as RedeemerBuilder from "../RedeemerBuilder.js" -import { BuildOptionsTag, TransactionBuilderError, TxBuilderConfigTag, TxContext } from "../TransactionBuilder.js" +import { FullProtocolParametersTag, TransactionBuilderError, type TxBuilderConfigTag,TxContext } from "../TransactionBuilder.js" import type { AuthCommitteeHotParams, DeregisterDRepParams, @@ -33,11 +33,10 @@ import type { */ export const createRegisterDRepProgram = ( params: RegisterDRepParams -): Effect.Effect => +): Effect.Effect => Effect.gen(function* () { const ctx = yield* TxContext - const config = yield* TxBuilderConfigTag - const buildOptions = yield* BuildOptionsTag + const fullParams = yield* FullProtocolParametersTag const isScriptControlled = params.drepCredential._tag === "ScriptHash" @@ -49,23 +48,12 @@ export const createRegisterDRepProgram = ( ) } - const protocolParams = yield* buildOptions.fullProtocolParameters - ? Effect.succeed(buildOptions.fullProtocolParameters) - : !config.provider - ? Effect.fail( - new TransactionBuilderError({ - message: "Provider required to fetch drepDeposit for DRep registration" - }) - ) - : config.provider.effect.getProtocolParameters().pipe( - Effect.mapError( - (err) => - new TransactionBuilderError({ - message: `Failed to fetch protocol parameters: ${err.message}` - }) - ) - ) - const drepDeposit = protocolParams.drepDeposit + if (!fullParams) { + return yield* Effect.fail( + new TransactionBuilderError({ message: "Provider required to fetch protocol parameters for DRep registration" }) + ) + } + const drepDeposit = fullParams.drepDeposit // Create RegDrepCert certificate with deposit const certificate = new Certificate.RegDrepCert({ @@ -193,11 +181,10 @@ export const createUpdateDRepProgram = ( */ export const createDeregisterDRepProgram = ( params: DeregisterDRepParams -): Effect.Effect => +): Effect.Effect => Effect.gen(function* () { const ctx = yield* TxContext - const config = yield* TxBuilderConfigTag - const buildOptions = yield* BuildOptionsTag + const fullParams = yield* FullProtocolParametersTag const isScriptControlled = params.drepCredential._tag === "ScriptHash" @@ -209,23 +196,12 @@ export const createDeregisterDRepProgram = ( ) } - const protocolParams = yield* buildOptions.fullProtocolParameters - ? Effect.succeed(buildOptions.fullProtocolParameters) - : !config.provider - ? Effect.fail( - new TransactionBuilderError({ - message: "Provider required to fetch drepDeposit for DRep deregistration" - }) - ) - : config.provider.effect.getProtocolParameters().pipe( - Effect.mapError( - (err) => - new TransactionBuilderError({ - message: `Failed to fetch protocol parameters: ${err.message}` - }) - ) - ) - const drepDeposit = protocolParams.drepDeposit + if (!fullParams) { + return yield* Effect.fail( + new TransactionBuilderError({ message: "Provider required to fetch protocol parameters for DRep deregistration" }) + ) + } + const drepDeposit = fullParams.drepDeposit // Create UnregDrepCert certificate with deposit refund const certificate = new Certificate.UnregDrepCert({ diff --git a/packages/evolution/src/sdk/builders/operations/Pool.ts b/packages/evolution/src/sdk/builders/operations/Pool.ts index d74dd9af..0ce9addc 100644 --- a/packages/evolution/src/sdk/builders/operations/Pool.ts +++ b/packages/evolution/src/sdk/builders/operations/Pool.ts @@ -9,7 +9,7 @@ import { Effect, Ref } from "effect" import * as Certificate from "../../../Certificate.js" import * as PoolKeyHash from "../../../PoolKeyHash.js" -import { BuildOptionsTag, TransactionBuilderError, TxBuilderConfigTag, TxContext } from "../TransactionBuilder.js" +import { FullProtocolParametersTag, TransactionBuilderError, type TxBuilderConfigTag,TxContext } from "../TransactionBuilder.js" import type { RegisterPoolParams, RetirePoolParams } from "./Operations.js" // ============================================================================ @@ -26,29 +26,17 @@ import type { RegisterPoolParams, RetirePoolParams } from "./Operations.js" */ export const createRegisterPoolProgram = ( params: RegisterPoolParams -): Effect.Effect => +): Effect.Effect => Effect.gen(function* () { const ctx = yield* TxContext - const config = yield* TxBuilderConfigTag - const buildOptions = yield* BuildOptionsTag + const fullParams = yield* FullProtocolParametersTag - const protocolParams = yield* buildOptions.fullProtocolParameters - ? Effect.succeed(buildOptions.fullProtocolParameters) - : !config.provider - ? Effect.fail( - new TransactionBuilderError({ - message: "Provider required to fetch poolDeposit for pool registration" - }) - ) - : config.provider.effect.getProtocolParameters().pipe( - Effect.mapError( - (err) => - new TransactionBuilderError({ - message: `Failed to fetch protocol parameters: ${err.message}` - }) - ) - ) - const poolDeposit = protocolParams.poolDeposit + if (!fullParams) { + return yield* Effect.fail( + new TransactionBuilderError({ message: "Provider required to fetch protocol parameters for pool registration" }) + ) + } + const poolDeposit = fullParams.poolDeposit // Create PoolRegistration certificate const certificate = new Certificate.PoolRegistration({ diff --git a/packages/evolution/src/sdk/builders/operations/Propose.ts b/packages/evolution/src/sdk/builders/operations/Propose.ts index 98237105..6dfcf7aa 100644 --- a/packages/evolution/src/sdk/builders/operations/Propose.ts +++ b/packages/evolution/src/sdk/builders/operations/Propose.ts @@ -9,7 +9,7 @@ import { Effect, Ref } from "effect" import * as ProposalProcedure from "../../../ProposalProcedure.js" import * as ProposalProcedures from "../../../ProposalProcedures.js" -import { BuildOptionsTag, TransactionBuilderError, TxBuilderConfigTag, TxContext } from "../TransactionBuilder.js" +import { FullProtocolParametersTag, TransactionBuilderError, type TxBuilderConfigTag,TxContext } from "../TransactionBuilder.js" import type { ProposeParams } from "./Operations.js" /** @@ -29,29 +29,17 @@ import type { ProposeParams } from "./Operations.js" */ export const createProposeProgram = ( params: ProposeParams -): Effect.Effect => +): Effect.Effect => Effect.gen(function* () { const ctx = yield* TxContext - const config = yield* TxBuilderConfigTag - const buildOptions = yield* BuildOptionsTag + const fullParams = yield* FullProtocolParametersTag - const protocolParams = yield* buildOptions.fullProtocolParameters - ? Effect.succeed(buildOptions.fullProtocolParameters) - : !config.provider - ? Effect.fail( - new TransactionBuilderError({ - message: "Provider required to fetch govActionDeposit for governance proposal" - }) - ) - : config.provider.effect.getProtocolParameters().pipe( - Effect.mapError( - (err) => - new TransactionBuilderError({ - message: `Failed to fetch protocol parameters: ${err.message}` - }) - ) - ) - const govActionDeposit = protocolParams.govActionDeposit + if (!fullParams) { + return yield* Effect.fail( + new TransactionBuilderError({ message: "Provider required to fetch protocol parameters for governance proposal" }) + ) + } + const govActionDeposit = fullParams.govActionDeposit // 2. Construct ProposalProcedure with fetched deposit const proposalProcedure = new ProposalProcedure.ProposalProcedure({ diff --git a/packages/evolution/src/sdk/builders/operations/Stake.ts b/packages/evolution/src/sdk/builders/operations/Stake.ts index c50c47e3..667f3b41 100644 --- a/packages/evolution/src/sdk/builders/operations/Stake.ts +++ b/packages/evolution/src/sdk/builders/operations/Stake.ts @@ -11,7 +11,7 @@ import * as Bytes from "../../../Bytes.js" import * as Certificate from "../../../Certificate.js" import * as RewardAccount from "../../../RewardAccount.js" import * as RedeemerBuilder from "../RedeemerBuilder.js" -import { BuildOptionsTag, TransactionBuilderError, TxBuilderConfigTag, TxContext } from "../TransactionBuilder.js" +import { FullProtocolParametersTag, TransactionBuilderError, TxBuilderConfigTag, TxContext } from "../TransactionBuilder.js" import type { DelegateToDRepParams, DelegateToParams, @@ -33,11 +33,10 @@ import type { */ export const createRegisterStakeProgram = ( params: RegisterStakeParams -): Effect.Effect => +): Effect.Effect => Effect.gen(function* () { const ctx = yield* TxContext - const config = yield* TxBuilderConfigTag - const buildOptions = yield* BuildOptionsTag + const fullParams = yield* FullProtocolParametersTag const isScriptControlled = params.stakeCredential._tag === "ScriptHash" @@ -49,23 +48,12 @@ export const createRegisterStakeProgram = ( ) } - const protocolParams = yield* buildOptions.fullProtocolParameters - ? Effect.succeed(buildOptions.fullProtocolParameters) - : !config.provider - ? Effect.fail( - new TransactionBuilderError({ - message: "Provider required to fetch keyDeposit for stake registration" - }) - ) - : config.provider.effect.getProtocolParameters().pipe( - Effect.mapError( - (err) => - new TransactionBuilderError({ - message: `Failed to fetch protocol parameters: ${err.message}` - }) - ) - ) - const keyDeposit = protocolParams.keyDeposit + if (!fullParams) { + return yield* Effect.fail( + new TransactionBuilderError({ message: "Provider required to fetch protocol parameters for stake registration" }) + ) + } + const keyDeposit = fullParams.keyDeposit // Create RegCert (Conway-era) certificate with deposit const certificate = new Certificate.RegCert({ @@ -386,11 +374,10 @@ export const createDelegateToPoolAndDRepProgram = ( */ export const createRegisterAndDelegateToProgram = ( params: RegisterAndDelegateToParams -): Effect.Effect => +): Effect.Effect => Effect.gen(function* () { const ctx = yield* TxContext - const config = yield* TxBuilderConfigTag - const buildOptions = yield* BuildOptionsTag + const fullParams = yield* FullProtocolParametersTag if (!params.poolKeyHash && !params.drep) { return yield* Effect.fail( @@ -400,23 +387,12 @@ export const createRegisterAndDelegateToProgram = ( ) } - const protocolParams = yield* buildOptions.fullProtocolParameters - ? Effect.succeed(buildOptions.fullProtocolParameters) - : !config.provider - ? Effect.fail( - new TransactionBuilderError({ - message: "Provider required to fetch keyDeposit for stake registration" - }) - ) - : config.provider.effect.getProtocolParameters().pipe( - Effect.mapError( - (err) => - new TransactionBuilderError({ - message: `Failed to fetch protocol parameters: ${err.message}` - }) - ) - ) - const keyDeposit = protocolParams.keyDeposit + if (!fullParams) { + return yield* Effect.fail( + new TransactionBuilderError({ message: "Provider required to fetch protocol parameters for stake registration and delegation" }) + ) + } + const keyDeposit = fullParams.keyDeposit // Check if script-controlled const isScriptControlled = params.stakeCredential._tag === "ScriptHash" @@ -514,11 +490,10 @@ export const createRegisterAndDelegateToProgram = ( */ export const createDeregisterStakeProgram = ( params: DeregisterStakeParams -): Effect.Effect => +): Effect.Effect => Effect.gen(function* () { const ctx = yield* TxContext - const config = yield* TxBuilderConfigTag - const buildOptions = yield* BuildOptionsTag + const fullParams = yield* FullProtocolParametersTag const isScriptControlled = params.stakeCredential._tag === "ScriptHash" @@ -530,23 +505,12 @@ export const createDeregisterStakeProgram = ( ) } - const protocolParams = yield* buildOptions.fullProtocolParameters - ? Effect.succeed(buildOptions.fullProtocolParameters) - : !config.provider - ? Effect.fail( - new TransactionBuilderError({ - message: "Provider required to fetch keyDeposit for stake deregistration" - }) - ) - : config.provider.effect.getProtocolParameters().pipe( - Effect.mapError( - (err) => - new TransactionBuilderError({ - message: `Failed to fetch protocol parameters: ${err.message}` - }) - ) - ) - const keyDeposit = protocolParams.keyDeposit + if (!fullParams) { + return yield* Effect.fail( + new TransactionBuilderError({ message: "Provider required to fetch protocol parameters for stake deregistration" }) + ) + } + const keyDeposit = fullParams.keyDeposit // Create UnregCert (Conway-era) certificate with deposit refund const certificate = new Certificate.UnregCert({ diff --git a/packages/evolution/src/sdk/builders/phases/Evaluation.ts b/packages/evolution/src/sdk/builders/phases/Evaluation.ts index 6bc917ba..e3f09bdd 100644 --- a/packages/evolution/src/sdk/builders/phases/Evaluation.ts +++ b/packages/evolution/src/sdk/builders/phases/Evaluation.ts @@ -19,22 +19,20 @@ import type * as Provider from "../../provider/Provider.js" import * as EvaluationStateManager from "../EvaluationStateManager.js" import { assembleTransaction } from "../internal/txBuilder.js" import type { IndexedInput } from "../RedeemerBuilder.js" -import type { - DeferredRedeemerData, - EvaluationContext, - PhaseResult, - RedeemerData, - ScriptFailure -} from "../TransactionBuilder.js" import { BuildOptionsTag, + type DeferredRedeemerData, + type EvaluationContext, EvaluationError, + FullProtocolParametersTag, PhaseContextTag, + type PhaseResult, + type RedeemerData, + type ScriptFailure, TransactionBuilderError, - TxBuilderConfigTag, + type TxBuilderConfigTag, TxContext, - voterToKey -} from "../TransactionBuilder.js" + voterToKey} from "../TransactionBuilder.js" /** * Convert ProtocolParameters cost models to CostModels core type for evaluation. @@ -262,7 +260,7 @@ const resolveDeferredRedeemers = ( export const executeEvaluation = (): Effect.Effect< PhaseResult, TransactionBuilderError, - BuildOptionsTag | TxContext | PhaseContextTag | TxBuilderConfigTag + BuildOptionsTag | FullProtocolParametersTag | TxContext | PhaseContextTag | TxBuilderConfigTag > => Effect.gen(function* () { yield* Effect.logDebug("[Evaluation] Starting UPLC evaluation") @@ -270,9 +268,9 @@ export const executeEvaluation = (): Effect.Effect< // Step 1: Get contexts const ctx = yield* TxContext const buildOptions = yield* BuildOptionsTag + const fullParamsOrUndefined = yield* FullProtocolParametersTag const buildCtxRef = yield* PhaseContextTag const buildCtx = yield* Ref.get(buildCtxRef) - const config = yield* TxBuilderConfigTag const state = yield* Ref.get(ctx) // Step 2: Get evaluator from BuildOptions or fail @@ -291,25 +289,15 @@ export const executeEvaluation = (): Effect.Effect< ) } - const fullProtocolParams = yield* buildOptions.fullProtocolParameters - ? Effect.succeed(buildOptions.fullProtocolParameters) - : !config.provider - ? Effect.fail( - new TransactionBuilderError({ - message: - "Script evaluation requires a provider to fetch full protocol parameters (cost models, execution limits)", - cause: { redeemerCount: state.redeemers.size } - }) - ) - : config.provider.effect.getProtocolParameters().pipe( - Effect.mapError( - (providerError) => - new TransactionBuilderError({ - message: `Failed to fetch full protocol parameters for evaluation: ${providerError.message}`, - cause: providerError - }) - ) - ) + if (!fullParamsOrUndefined) { + return yield* Effect.fail( + new TransactionBuilderError({ + message: "Provider required to fetch protocol parameters for script evaluation", + cause: { redeemerCount: state.redeemers.size } + }) + ) + } + const fullProtocolParams = fullParamsOrUndefined // Step 3: Check if there are redeemers to evaluate (resolved or deferred) const hasResolvedRedeemers = state.redeemers.size > 0 diff --git a/packages/evolution/test/TxBuilder.ProtocolParamsOverride.test.ts b/packages/evolution/test/TxBuilder.ProtocolParamsOverride.test.ts index e4f002f5..ed3ad6d8 100644 --- a/packages/evolution/test/TxBuilder.ProtocolParamsOverride.test.ts +++ b/packages/evolution/test/TxBuilder.ProtocolParamsOverride.test.ts @@ -5,7 +5,7 @@ import * as Address from "../src/Address.js" import * as Credential from "../src/Credential.js" import { makeTxBuilder } from "../src/sdk/builders/TransactionBuilder.js" import { mainnet } from "../src/sdk/client/index.js" -import type { Provider as ProviderType } from "../src/sdk/provider/Provider.js" +import type { ProtocolParameters, Provider } from "../src/sdk/provider/Provider.js" import type * as CoreUTxO from "../src/UTxO.js" import { createCoreTestUtxo } from "./utils/utxo-helpers.js" @@ -34,7 +34,7 @@ const FULL_PROTOCOL_PARAMS = { PlutusV2: {} as Record, PlutusV3: {} as Record } -} satisfies ProviderType["effect"] extends { getProtocolParameters: () => Effect.Effect } ? P : never +} satisfies ProtocolParameters const PROTOCOL_PARAMS_FOR_FEE = { minFeeCoefficient: 44n, @@ -78,7 +78,7 @@ const makeSpyProvider = () => { evaluateTx: notImpl("evaluateTx") } - const provider = { effect } as unknown as ProviderType + const provider = { effect } as unknown as Provider return { provider, getCallCount: () => callCount } } @@ -141,7 +141,7 @@ describe("fullProtocolParameters override — registerStake", () => { availableUtxos: utxos, protocolParameters: PROTOCOL_PARAMS_FOR_FEE }) - ).rejects.toThrow(/Provider required to fetch keyDeposit for stake registration/) + ).rejects.toThrow(/Provider required to fetch protocol parameters for stake registration/) }) }) @@ -186,7 +186,7 @@ describe("fullProtocolParameters override — deregisterStake", () => { availableUtxos: utxos, protocolParameters: PROTOCOL_PARAMS_FOR_FEE }) - ).rejects.toThrow(/Provider required to fetch keyDeposit for stake deregistration/) + ).rejects.toThrow(/Provider required to fetch protocol parameters for stake deregistration/) }) }) @@ -246,7 +246,7 @@ describe("fullProtocolParameters override — registerDRep", () => { availableUtxos: utxos, protocolParameters: PROTOCOL_PARAMS_FOR_FEE }) - ).rejects.toThrow(/Provider required to fetch drepDeposit for DRep registration/) + ).rejects.toThrow(/Provider required to fetch protocol parameters for DRep registration/) }) }) @@ -291,6 +291,6 @@ describe("fullProtocolParameters override — deregisterDRep", () => { availableUtxos: utxos, protocolParameters: PROTOCOL_PARAMS_FOR_FEE }) - ).rejects.toThrow(/Provider required to fetch drepDeposit for DRep deregistration/) + ).rejects.toThrow(/Provider required to fetch protocol parameters for DRep deregistration/) }) }) From d23639872d3f8e46ea9c4bf7dd67f65c1f44c4cf Mon Sep 17 00:00:00 2001 From: solidsnakedev Date: Tue, 28 Apr 2026 18:07:46 -0600 Subject: [PATCH 4/4] release: changeset for fullProtocolParameters --- .changeset/full-protocol-parameters.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/full-protocol-parameters.md diff --git a/.changeset/full-protocol-parameters.md b/.changeset/full-protocol-parameters.md new file mode 100644 index 00000000..81be20f2 --- /dev/null +++ b/.changeset/full-protocol-parameters.md @@ -0,0 +1,5 @@ +--- +"@evolution-sdk/evolution": patch +--- + +Add `fullProtocolParameters` to `BuildOptions` for providerless transaction builds