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 diff --git a/packages/evolution/src/sdk/builders/TransactionBuilder.ts b/packages/evolution/src/sdk/builders/TransactionBuilder.ts index 2e22c01b..a1569c40 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. * @@ -632,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. * @@ -669,7 +703,11 @@ export class BuildOptionsTag extends Context.Tag("BuildOptions") +export type ProgramStep = Effect.Effect< + void, + TransactionBuilderError, + TxContext | TxBuilderConfigTag | BuildOptionsTag | FullProtocolParametersTag +> // ============================================================================ // Voter Key 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 a6585496..d3814cc2 100644 --- a/packages/evolution/src/sdk/builders/internal/resolve.ts +++ b/packages/evolution/src/sdk/builders/internal/resolve.ts @@ -19,6 +19,19 @@ export const resolveProtocolParameters = ( config: TxBuilderConfig, options?: BuildOptions ): Effect.Effect => { + 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 + }) + } + if (options?.protocolParameters !== undefined) { return Effect.succeed(options.protocolParameters) } @@ -47,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 0c2bb87a..0f9ff847 100644 --- a/packages/evolution/src/sdk/builders/internal/txBuilder.ts +++ b/packages/evolution/src/sdk/builders/internal/txBuilder.ts @@ -34,8 +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, TransactionBuilderError, TxBuilderConfigTag, TxContext, voterToKey } from "../TransactionBuilder.js" +import { + BuildOptionsTag, + FullProtocolParametersTag, + TransactionBuilderError, + type TxBuilderConfigTag, + TxContext, + type UnfrackOptions, + voterToKey} from "../TransactionBuilder.js" import * as Unfrack from "../Unfrack.js" // ============================================================================ @@ -276,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 @@ -526,29 +532,17 @@ 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 + const fullParamsOrUndefined = yield* FullProtocolParametersTag - if (!config.provider) { + if (!fullParamsOrUndefined) { return yield* Effect.fail( new TransactionBuilderError({ - message: - "Script transactions require a provider to fetch full protocol parameters for scriptDataHash calculation", + message: "Provider required to fetch 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 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 @@ -739,9 +733,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..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 { TransactionBuilderError, TxBuilderConfigTag, TxContext } from "../TransactionBuilder.js" +import { FullProtocolParametersTag, TransactionBuilderError, type TxBuilderConfigTag,TxContext } from "../TransactionBuilder.js" import type { AuthCommitteeHotParams, DeregisterDRepParams, @@ -33,16 +33,13 @@ import type { */ export const createRegisterDRepProgram = ( params: RegisterDRepParams -): Effect.Effect => +): Effect.Effect => Effect.gen(function* () { const ctx = yield* TxContext - const config = yield* TxBuilderConfigTag + const fullParams = yield* FullProtocolParametersTag - // 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,24 +48,12 @@ export const createRegisterDRepProgram = ( ) } - // Get drepDeposit from protocol parameters via provider - if (!config.provider) { + if (!fullParams) { return yield* Effect.fail( - new TransactionBuilderError({ - message: "Provider required to fetch drepDeposit for DRep registration" - }) + new TransactionBuilderError({ message: "Provider required to fetch protocol parameters 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 drepDeposit = protocolParams.drepDeposit + const drepDeposit = fullParams.drepDeposit // Create RegDrepCert certificate with deposit const certificate = new Certificate.RegDrepCert({ @@ -196,21 +181,11 @@ export const createUpdateDRepProgram = ( */ export const createDeregisterDRepProgram = ( params: DeregisterDRepParams -): Effect.Effect => +): Effect.Effect => Effect.gen(function* () { const ctx = yield* TxContext - const config = yield* TxBuilderConfigTag - - // 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" - }) - ) - } + const fullParams = yield* FullProtocolParametersTag - // Check if script-controlled const isScriptControlled = params.drepCredential._tag === "ScriptHash" if (isScriptControlled && !params.redeemer) { @@ -221,15 +196,12 @@ export const createDeregisterDRepProgram = ( ) } - const protocolParams = yield* config.provider.effect.getProtocolParameters().pipe( - Effect.mapError( - (err) => - new TransactionBuilderError({ - message: `Failed to fetch protocol parameters: ${err.message}` - }) + if (!fullParams) { + return yield* Effect.fail( + new TransactionBuilderError({ message: "Provider required to fetch protocol parameters for DRep deregistration" }) ) - ) - const drepDeposit = protocolParams.drepDeposit + } + 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 3b1855ec..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 { TransactionBuilderError, TxBuilderConfigTag, TxContext } from "../TransactionBuilder.js" +import { FullProtocolParametersTag, TransactionBuilderError, type TxBuilderConfigTag,TxContext } from "../TransactionBuilder.js" import type { RegisterPoolParams, RetirePoolParams } from "./Operations.js" // ============================================================================ @@ -26,31 +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 fullParams = yield* FullProtocolParametersTag - // 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) { + if (!fullParams) { return yield* Effect.fail( - new TransactionBuilderError({ - message: "Provider required to fetch poolDeposit for pool registration" - }) + new TransactionBuilderError({ message: "Provider required to fetch protocol parameters 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 poolDeposit = protocolParams.poolDeposit + 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 89f80d3b..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 { 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 fullParams = yield* FullProtocolParametersTag - // 1. Get govActionDeposit from protocol parameters via provider - if (!config.provider) { + if (!fullParams) { return yield* Effect.fail( - new TransactionBuilderError({ - message: "Provider required to fetch govActionDeposit for governance proposal" - }) + new TransactionBuilderError({ message: "Provider required to fetch protocol parameters 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 govActionDeposit = protocolParams.govActionDeposit + 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 7ae1e2e3..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 { TransactionBuilderError, TxBuilderConfigTag, TxContext } from "../TransactionBuilder.js" +import { FullProtocolParametersTag, TransactionBuilderError, TxBuilderConfigTag, TxContext } from "../TransactionBuilder.js" import type { DelegateToDRepParams, DelegateToParams, @@ -33,21 +33,11 @@ import type { */ export const createRegisterStakeProgram = ( params: RegisterStakeParams -): Effect.Effect => +): Effect.Effect => Effect.gen(function* () { const ctx = yield* TxContext - const config = yield* TxBuilderConfigTag - - // 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 fullParams = yield* FullProtocolParametersTag - // Check if script-controlled const isScriptControlled = params.stakeCredential._tag === "ScriptHash" if (isScriptControlled && !params.redeemer) { @@ -58,15 +48,12 @@ export const createRegisterStakeProgram = ( ) } - const protocolParams = yield* config.provider.effect.getProtocolParameters().pipe( - Effect.mapError( - (err) => - new TransactionBuilderError({ - message: `Failed to fetch protocol parameters: ${err.message}` - }) + if (!fullParams) { + return yield* Effect.fail( + new TransactionBuilderError({ message: "Provider required to fetch protocol parameters for stake registration" }) ) - ) - const keyDeposit = protocolParams.keyDeposit + } + const keyDeposit = fullParams.keyDeposit // Create RegCert (Conway-era) certificate with deposit const certificate = new Certificate.RegCert({ @@ -387,12 +374,11 @@ export const createDelegateToPoolAndDRepProgram = ( */ export const createRegisterAndDelegateToProgram = ( params: RegisterAndDelegateToParams -): Effect.Effect => +): Effect.Effect => Effect.gen(function* () { const ctx = yield* TxContext - const config = yield* TxBuilderConfigTag + const fullParams = yield* FullProtocolParametersTag - // Validate at least one delegation target if (!params.poolKeyHash && !params.drep) { return yield* Effect.fail( new TransactionBuilderError({ @@ -401,24 +387,12 @@ export const createRegisterAndDelegateToProgram = ( ) } - // Get keyDeposit from protocol parameters via provider - if (!config.provider) { + if (!fullParams) { return yield* Effect.fail( - new TransactionBuilderError({ - message: "Provider required to fetch keyDeposit for stake registration" - }) + new TransactionBuilderError({ message: "Provider required to fetch protocol parameters for stake registration and delegation" }) ) } - - const protocolParams = yield* config.provider.effect.getProtocolParameters().pipe( - Effect.mapError( - (err) => - new TransactionBuilderError({ - message: `Failed to fetch protocol parameters: ${err.message}` - }) - ) - ) - const keyDeposit = protocolParams.keyDeposit + const keyDeposit = fullParams.keyDeposit // Check if script-controlled const isScriptControlled = params.stakeCredential._tag === "ScriptHash" @@ -516,12 +490,11 @@ export const createRegisterAndDelegateToProgram = ( */ export const createDeregisterStakeProgram = ( params: DeregisterStakeParams -): Effect.Effect => +): Effect.Effect => Effect.gen(function* () { const ctx = yield* TxContext - const config = yield* TxBuilderConfigTag + const fullParams = yield* FullProtocolParametersTag - // Check if script-controlled const isScriptControlled = params.stakeCredential._tag === "ScriptHash" if (isScriptControlled && !params.redeemer) { @@ -532,24 +505,12 @@ export const createDeregisterStakeProgram = ( ) } - // Get keyDeposit from protocol parameters via provider - if (!config.provider) { + if (!fullParams) { return yield* Effect.fail( - new TransactionBuilderError({ - message: "Provider required to fetch keyDeposit for stake deregistration" - }) + new TransactionBuilderError({ message: "Provider required to fetch protocol parameters 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 keyDeposit = protocolParams.keyDeposit + 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 2bb41dde..e3f09bdd 100644 --- a/packages/evolution/src/sdk/builders/phases/Evaluation.ts +++ b/packages/evolution/src/sdk/builders/phases/Evaluation.ts @@ -19,8 +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, EvaluationError, PhaseContextTag, TransactionBuilderError, TxBuilderConfigTag, TxContext, voterToKey } from "../TransactionBuilder.js" +import { + BuildOptionsTag, + type DeferredRedeemerData, + type EvaluationContext, + EvaluationError, + FullProtocolParametersTag, + PhaseContextTag, + type PhaseResult, + type RedeemerData, + type ScriptFailure, + TransactionBuilderError, + type TxBuilderConfigTag, + TxContext, + voterToKey} from "../TransactionBuilder.js" /** * Convert ProtocolParameters cost models to CostModels core type for evaluation. @@ -248,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") @@ -256,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 @@ -277,26 +289,15 @@ export const executeEvaluation = (): Effect.Effect< ) } - // Step 2.5: Fetch full protocol parameters (needed for cost models and execution limits) - if (!config.provider) { + if (!fullParamsOrUndefined) { return yield* Effect.fail( new TransactionBuilderError({ - message: - "Script evaluation requires a provider to fetch full protocol parameters (cost models, execution limits)", + message: "Provider required to fetch protocol parameters for script evaluation", 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 = 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 new file mode 100644 index 00000000..ed3ad6d8 --- /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 { makeTxBuilder } from "../src/sdk/builders/TransactionBuilder.js" +import { mainnet } from "../src/sdk/client/index.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" + +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 ProtocolParameters + +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 Provider + + 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 protocol parameters 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 protocol parameters 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 protocol parameters 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 protocol parameters for DRep deregistration/) + }) +})