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

Filter by extension

Filter by extension

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

Add `fullProtocolParameters` to `BuildOptions` for providerless transaction builds
42 changes: 40 additions & 2 deletions packages/evolution/src/sdk/builders/TransactionBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -669,7 +703,11 @@ export class BuildOptionsTag extends Context.Tag("BuildOptions")<BuildOptionsTag
* @since 2.0.0
* @category model
*/
export type ProgramStep = Effect.Effect<void, TransactionBuilderError, TxContext | TxBuilderConfigTag>
export type ProgramStep = Effect.Effect<
void,
TransactionBuilderError,
TxContext | TxBuilderConfigTag | BuildOptionsTag | FullProtocolParametersTag
>

// ============================================================================
// Voter Key
Expand Down
3 changes: 2 additions & 1 deletion packages/evolution/src/sdk/builders/internal/layers.ts
Original file line number Diff line number Diff line change
@@ -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"

Expand Down Expand Up @@ -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))
)
Expand Down
44 changes: 44 additions & 0 deletions packages/evolution/src/sdk/builders/internal/resolve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,19 @@ export const resolveProtocolParameters = (
config: TxBuilderConfig,
options?: BuildOptions
): Effect.Effect<ProtocolParameters, TransactionBuilderError | Provider.ProviderError> => {
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)
}
Expand Down Expand Up @@ -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<Provider.ProtocolParameters | undefined, TransactionBuilderError> => {
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.
*
Expand Down
37 changes: 15 additions & 22 deletions packages/evolution/src/sdk/builders/internal/txBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"

// ============================================================================
Expand Down Expand Up @@ -276,7 +282,7 @@ export const assembleTransaction = (
inputs: ReadonlyArray<TransactionInput.TransactionInput>,
outputs: ReadonlyArray<TxOut.TransactionOutput>,
fee: bigint
): Effect.Effect<Transaction.Transaction, TransactionBuilderError, TxContext | TxBuilderConfigTag | BuildOptionsTag> =>
): Effect.Effect<Transaction.Transaction, TransactionBuilderError, TxContext | TxBuilderConfigTag | BuildOptionsTag | FullProtocolParametersTag> =>
Effect.gen(function* () {
// Get state ref to access scripts and redeemers
const stateRef = yield* TxContext
Expand Down Expand Up @@ -526,29 +532,17 @@ export const assembleTransaction = (
let scriptDataHash: ReturnType<typeof Redeemers.toScriptDataHash> | 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
Expand Down Expand Up @@ -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.
Expand Down
54 changes: 13 additions & 41 deletions packages/evolution/src/sdk/builders/operations/Governance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -33,16 +33,13 @@ import type {
*/
export const createRegisterDRepProgram = (
params: RegisterDRepParams
): Effect.Effect<void, TransactionBuilderError, TxContext | TxBuilderConfigTag> =>
): Effect.Effect<void, TransactionBuilderError, TxContext | TxBuilderConfigTag | FullProtocolParametersTag> =>
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({
Expand All @@ -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({
Expand Down Expand Up @@ -196,21 +181,11 @@ export const createUpdateDRepProgram = (
*/
export const createDeregisterDRepProgram = (
params: DeregisterDRepParams
): Effect.Effect<void, TransactionBuilderError, TxContext | TxBuilderConfigTag> =>
): Effect.Effect<void, TransactionBuilderError, TxContext | TxBuilderConfigTag | FullProtocolParametersTag> =>
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) {
Expand All @@ -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({
Expand Down
26 changes: 6 additions & 20 deletions packages/evolution/src/sdk/builders/operations/Pool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"

// ============================================================================
Expand All @@ -26,31 +26,17 @@ import type { RegisterPoolParams, RetirePoolParams } from "./Operations.js"
*/
export const createRegisterPoolProgram = (
params: RegisterPoolParams
): Effect.Effect<void, TransactionBuilderError, TxContext | TxBuilderConfigTag> =>
): Effect.Effect<void, TransactionBuilderError, TxContext | TxBuilderConfigTag | FullProtocolParametersTag> =>
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({
Expand Down
Loading
Loading