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..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, TxBuilderConfigTag, TxContext } from "../TransactionBuilder.js" +import { BuildOptionsTag, TransactionBuilderError, 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..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, TxBuilderConfigTag, TxContext } from "../TransactionBuilder.js" +import { BuildOptionsTag, TransactionBuilderError, 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..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, TxBuilderConfigTag, TxContext } from "../TransactionBuilder.js" +import { BuildOptionsTag, TransactionBuilderError, 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..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, TxBuilderConfigTag, TxContext } from "../TransactionBuilder.js" +import { BuildOptionsTag, TransactionBuilderError, 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..e4f002f5 --- /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 { 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 = + "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/) + }) +})