diff --git a/packages/evolution/docgen.json b/packages/evolution/docgen.json index f298abee..cc0cc8c8 100644 --- a/packages/evolution/docgen.json +++ b/packages/evolution/docgen.json @@ -12,7 +12,7 @@ "skipExamplesValidation": true, "skipModulesValidation": true, "exclude": ["**/*.test.ts", "**/*.spec.ts", "**/test/**", "**/tests/**", "**/internal/**", "**/index.ts", "**/debug/**"], - "projectHomepage": "https://github.com/no-witness-labs/evolution-sdk", + "projectHomepage": "https://github.com/IntersectMBO/evolution-sdk", "docsHomepage": "TBD", "projectName": "Evolution SDK", "packageName": "@evolution-sdk/evolution" diff --git a/packages/evolution/package.json b/packages/evolution/package.json index b7012fba..e2a30f2f 100644 --- a/packages/evolution/package.json +++ b/packages/evolution/package.json @@ -65,13 +65,13 @@ "sdk", "web3" ], - "homepage": "https://github.com/no-witness-labs/evolution-sdk", + "homepage": "https://github.com/IntersectMBO/evolution-sdk", "repository": { "type": "git", - "url": "git+https://github.com/no-witness-labs/evolution-sdk.git" + "url": "git+https://github.com/IntersectMBO/evolution-sdk.git" }, "bugs": { - "url": "https://github.com/no-witness-labs/evolution-sdk/issues" + "url": "https://github.com/IntersectMBO/evolution-sdk/issues" }, "license": "MIT", "publishConfig": { diff --git a/packages/evolution/src/builders/CertificateBuilder.ts b/packages/evolution/src/builders/CertificateBuilder.ts deleted file mode 100644 index 8f01fa65..00000000 --- a/packages/evolution/src/builders/CertificateBuilder.ts +++ /dev/null @@ -1,269 +0,0 @@ -import { Data, Effect as Eff } from "effect" - -import type * as Certificate from "../core/Certificate.js" -import type * as Credential from "../core/Credential.js" -import * as KeyHash from "../core/KeyHash.js" -import type * as NativeScripts from "../core/NativeScripts.js" -import type * as PoolKeyHash from "../core/PoolKeyHash.js" -import * as ScriptHash from "../core/ScriptHash.js" -import type { NativeScriptWitnessInfo, PartialPlutusWitness } from "./WitnessBuilder.js" -import { InputAggregateWitnessData, PlutusScriptWitness, RequiredWitnessSet } from "./WitnessBuilder.js" - -/** - * Error class for CertificateBuilder related operations. - * - * @since 2.0.0 - * @category errors - */ -export class CertificateBuilderError extends Data.TaggedError("CertificateBuilderError")<{ - message?: string - cause?: unknown -}> {} - -/** - * Calculates required witnesses for a certificate - * - * @since 2.0.0 - * @category utils - */ -export function certRequiredWits(cert: Certificate.Certificate, requiredWitnesses: RequiredWitnessSet): void { - switch (cert._tag) { - case "StakeRegistration": - // Stake key registrations do not require a witness - break - - case "StakeDeregistration": - addCredentialWitness(cert.stakeCredential, requiredWitnesses) - break - - case "StakeDelegation": - addCredentialWitness(cert.stakeCredential, requiredWitnesses) - break - - case "PoolRegistration": - cert.poolParams.poolOwners.forEach((owner) => { - requiredWitnesses.addVkeyKeyHash(owner) // owner is already KeyHash - }) - requiredWitnesses.addVkeyKeyHash(poolKeyHashToKeyHash(cert.poolParams.operator)) // operator is PoolKeyHash - break - - case "PoolRetirement": - requiredWitnesses.addVkeyKeyHash(poolKeyHashToKeyHash(cert.poolKeyHash)) - break - - case "RegCert": - addCredentialWitness(cert.stakeCredential, requiredWitnesses) - break - - case "UnregCert": - addCredentialWitness(cert.stakeCredential, requiredWitnesses) - break - - case "VoteDelegCert": - addCredentialWitness(cert.stakeCredential, requiredWitnesses) - break - - case "StakeVoteDelegCert": - addCredentialWitness(cert.stakeCredential, requiredWitnesses) - break - - case "StakeRegDelegCert": - addCredentialWitness(cert.stakeCredential, requiredWitnesses) - break - - case "VoteRegDelegCert": - addCredentialWitness(cert.stakeCredential, requiredWitnesses) - break - - case "StakeVoteRegDelegCert": - addCredentialWitness(cert.stakeCredential, requiredWitnesses) - break - - case "AuthCommitteeHotCert": - addCredentialWitness(cert.committeeColdCredential, requiredWitnesses) - break - - case "ResignCommitteeColdCert": - addCredentialWitness(cert.committeeColdCredential, requiredWitnesses) - break - - case "RegDrepCert": - addCredentialWitness(cert.drepCredential, requiredWitnesses) - break - - case "UnregDrepCert": - addCredentialWitness(cert.drepCredential, requiredWitnesses) - break - - case "UpdateDrepCert": - addCredentialWitness(cert.drepCredential, requiredWitnesses) - break - } -} - -function addCredentialWitness(credential: Credential.CredentialSchema, requiredWitnesses: RequiredWitnessSet): void { - switch (credential._tag) { - case "KeyHash": - requiredWitnesses.addVkeyKeyHash(credential) - break - case "ScriptHash": - requiredWitnesses.addScriptHash(credential) - break - } -} - -function poolKeyHashToKeyHash(poolKeyHash: PoolKeyHash.PoolKeyHash): KeyHash.KeyHash { - // Both PoolKeyHash and KeyHash are based on Hash28, so we can convert by extracting the hash - return KeyHash.make({ hash: poolKeyHash.hash }) -} - -/** - * Result of building a certificate - * - * @since 2.0.0 - * @category model - */ -export interface CertificateBuilderResult { - cert: Certificate.Certificate - aggregateWitness?: InputAggregateWitnessData - requiredWits: RequiredWitnessSet -} - -/** - * Builder for a single certificate - * - * @since 2.0.0 - * @category builders - */ -export class SingleCertificateBuilder { - constructor(public readonly cert: Certificate.Certificate) {} - - static new(cert: Certificate.Certificate): SingleCertificateBuilder { - return new SingleCertificateBuilder(cert) - } - - skipWitness(): CertificateBuilderResult { - const requiredWits = RequiredWitnessSet.default() - certRequiredWits(this.cert, requiredWits) - - return { - cert: this.cert, - aggregateWitness: undefined, - requiredWits - } - } - - paymentKey(): Eff.Effect { - return Eff.gen( - function* (this: SingleCertificateBuilder) { - const requiredWits = RequiredWitnessSet.default() - certRequiredWits(this.cert, requiredWits) - - if (requiredWits.scripts.length > 0) { - return yield* Eff.fail( - new CertificateBuilderError({ - message: `Certificate contains script. Expected public key hash.` - }) - ) - } - - return { - cert: this.cert, - aggregateWitness: undefined, - requiredWits - } - }.bind(this) - ) - } - - nativeScript( - nativeScript: NativeScripts.NativeScript, - witnessInfo: NativeScriptWitnessInfo - ): Eff.Effect { - return Eff.gen( - function* (this: SingleCertificateBuilder) { - const requiredWits = RequiredWitnessSet.default() - certRequiredWits(this.cert, requiredWits) - const requiredWitsLeft = structuredClone(requiredWits) - - const scriptHash = ScriptHash.fromScript(nativeScript) - - // Check if the script is actually required - const contains = requiredWitsLeft.scripts.some((h) => ScriptHash.equals(h, scriptHash)) - - // Remove the script hash - const filteredScripts = requiredWitsLeft.scripts.filter((h) => !ScriptHash.equals(h, scriptHash)) - const mutableRequiredWitsLeft = { ...requiredWitsLeft, scripts: filteredScripts } - - if (mutableRequiredWitsLeft.scripts.length > 0) { - return yield* Eff.fail( - new CertificateBuilderError({ - message: "Missing the following witnesses for the certificate", - cause: mutableRequiredWitsLeft - }) - ) - } - - return { - cert: this.cert, - aggregateWitness: contains ? InputAggregateWitnessData.nativeScript(nativeScript, witnessInfo) : undefined, - requiredWits - } - }.bind(this) - ) - } - - plutusScript( - partialWitness: PartialPlutusWitness, - requiredSigners: Array - ): Eff.Effect { - return Eff.gen( - function* (this: SingleCertificateBuilder) { - const requiredWits = RequiredWitnessSet.default() - requiredSigners.forEach((signer) => requiredWits.addVkeyKeyHash(signer)) - certRequiredWits(this.cert, requiredWits) - const requiredWitsLeft = structuredClone(requiredWits) - - // Clear vkeys as we don't know which ones will be used - const mutableRequiredWitsLeft = { ...requiredWitsLeft, vkeys: [] } - - const scriptHash = PlutusScriptWitness.hash(partialWitness.scriptWitness) - - // Check if the script is actually required - const contains = requiredWitsLeft.scripts.some((h) => ScriptHash.equals(h, scriptHash)) - - // Remove the script hash - const filteredPlutusScripts = mutableRequiredWitsLeft.scripts.filter((h) => !ScriptHash.equals(h, scriptHash)) - const finalRequiredWitsLeft = new RequiredWitnessSet({ - vkeys: mutableRequiredWitsLeft.vkeys, - bootstraps: mutableRequiredWitsLeft.bootstraps, - scripts: filteredPlutusScripts, - plutusData: mutableRequiredWitsLeft.plutusData, - redeemers: mutableRequiredWitsLeft.redeemers, - scriptRefs: mutableRequiredWitsLeft.scriptRefs - }) - - if (finalRequiredWitsLeft.len() > 0) { - return yield* Eff.fail( - new CertificateBuilderError({ - message: "Missing the following witnesses for the certificate", - cause: finalRequiredWitsLeft - }) - ) - } - - return { - cert: this.cert, - aggregateWitness: contains - ? InputAggregateWitnessData.plutusScript( - partialWitness, - requiredSigners, - undefined // No datum for certificates - ) - : undefined, - requiredWits - } - }.bind(this) - ) - } -} diff --git a/packages/evolution/src/builders/InputBuilder.ts b/packages/evolution/src/builders/InputBuilder.ts deleted file mode 100644 index 6240f673..00000000 --- a/packages/evolution/src/builders/InputBuilder.ts +++ /dev/null @@ -1,248 +0,0 @@ -import { Data, Effect as Eff } from "effect" - -import type * as Credential from "../core/Credential.js" -import type * as PlutusData from "../core/Data.js" -import * as DatumOption from "../core/DatumOption.js" -import type * as KeyHash from "../core/KeyHash.js" -import type * as NativeScripts from "../core/NativeScripts.js" -import * as ScriptHash from "../core/ScriptHash.js" -import type * as TransactionInput from "../core/TransactionInput.js" -import type * as TransactionOutput from "../core/TransactionOutput.js" -import { hashPlutusData } from "../utils/Hash.js" -import type { NativeScriptWitnessInfo, PartialPlutusWitness } from "./WitnessBuilder.js" -import { InputAggregateWitnessData, PlutusScriptWitness, RequiredWitnessSet } from "./WitnessBuilder.js" - -/** - * Error class for InputBuilder related operations. - * - * @since 2.0.0 - * @category errors - */ -export class InputBuilderError extends Data.TaggedError("InputBuilderError")<{ - message?: string - cause?: unknown -}> {} - -/** - * Calculates required witnesses for a transaction output - * - * @since 2.0.0 - * @category utils - */ -export function inputRequiredWits( - utxoInfo: TransactionOutput.TransactionOutput, - requiredWitnesses: RequiredWitnessSet -): void { - const address = utxoInfo.address - - // Extract payment credential based on address type - // TransactionOutput only supports BaseAddress and EnterpriseAddress - let paymentCred: Credential.CredentialSchema | undefined - switch (address._tag) { - case "BaseAddress": - paymentCred = address.paymentCredential - break - case "EnterpriseAddress": - paymentCred = address.paymentCredential - break - } - - if (paymentCred) { - switch (paymentCred._tag) { - case "KeyHash": - requiredWitnesses.addVkeyKeyHash(paymentCred) - break - case "ScriptHash": - requiredWitnesses.addScriptHash(paymentCred) - // Check for datum hash in output - if (utxoInfo._tag === "ShelleyTransactionOutput" && utxoInfo.datumHash) { - requiredWitnesses.addPlutusDataHash(utxoInfo.datumHash) - } else if (utxoInfo._tag === "BabbageTransactionOutput" && utxoInfo.datumOption) { - if (utxoInfo.datumOption._tag === "DatumHash") { - requiredWitnesses.addPlutusDataHash(utxoInfo.datumOption) - } - } - break - } - } -} - -/** - * Result of building a transaction input - * - * @since 2.0.0 - * @category model - */ -export interface InputBuilderResult { - input: TransactionInput.TransactionInput - utxoInfo: TransactionOutput.TransactionOutput - aggregateWitness?: InputAggregateWitnessData - requiredWits: RequiredWitnessSet -} - -/** - * Builder for a single transaction input - * - * @since 2.0.0 - * @category builders - */ -export class SingleInputBuilder { - constructor( - public readonly input: TransactionInput.TransactionInput, - public readonly utxoInfo: TransactionOutput.TransactionOutput - ) {} - - static new( - input: TransactionInput.TransactionInput, - utxoInfo: TransactionOutput.TransactionOutput - ): SingleInputBuilder { - return new SingleInputBuilder(input, utxoInfo) - } - - paymentKey(): Eff.Effect { - return Eff.gen( - function* (this: SingleInputBuilder) { - const requiredWits = RequiredWitnessSet.default() - inputRequiredWits(this.utxoInfo, requiredWits) - - // Check that no scripts are required - if (requiredWits.scripts.length > 0) { - return yield* Eff.fail( - new InputBuilderError({ - message: `UTXO address was not a payment key: ${this.utxoInfo.address}` - }) - ) - } - - return { - input: this.input, - utxoInfo: this.utxoInfo, - aggregateWitness: undefined, - requiredWits - } - }.bind(this) - ) - } - - nativeScript( - nativeScript: NativeScripts.NativeScript, - witnessInfo: NativeScriptWitnessInfo - ): Eff.Effect { - return Eff.gen( - function* (this: SingleInputBuilder) { - const requiredWits = RequiredWitnessSet.default() - inputRequiredWits(this.utxoInfo, requiredWits) - const requiredWitsLeft = structuredClone(requiredWits) - - const scriptHash = ScriptHash.fromScript(nativeScript) - - // Remove the script hash from required witnesses - const filteredScripts = requiredWitsLeft.scripts.filter((h) => !ScriptHash.equals(h, scriptHash)) - const mutableRequiredWitsLeft = { ...requiredWitsLeft, scripts: filteredScripts } - - if (mutableRequiredWitsLeft.scripts.length > 0) { - return yield* Eff.fail( - new InputBuilderError({ - message: `Missing the following witnesses for the input`, - cause: mutableRequiredWitsLeft - }) - ) - } - - return { - input: this.input, - utxoInfo: this.utxoInfo, - aggregateWitness: InputAggregateWitnessData.nativeScript(nativeScript, witnessInfo), - requiredWits - } - }.bind(this) - ) - } - - plutusScript( - partialWitness: PartialPlutusWitness, - requiredSigners: Array, - datum: PlutusData.Data - ): Eff.Effect { - return this.plutusScriptInner(partialWitness, requiredSigners, datum) - } - - plutusScriptInlineDatum( - partialWitness: PartialPlutusWitness, - requiredSigners: Array - ): Eff.Effect { - return this.plutusScriptInner(partialWitness, requiredSigners, undefined) - } - - private plutusScriptInner( - partialWitness: PartialPlutusWitness, - requiredSigners: Array, - datum?: PlutusData.Data - ): Eff.Effect { - return Eff.gen( - function* (this: SingleInputBuilder) { - const requiredWits = RequiredWitnessSet.default() - - // Add required signers - requiredSigners.forEach((signer) => requiredWits.addVkeyKeyHash(signer)) - - inputRequiredWits(this.utxoInfo, requiredWits) - const requiredWitsLeft = structuredClone(requiredWits) - - // Clear vkeys as we don't know which ones will be used - const clearedRequiredWitsLeft = new RequiredWitnessSet({ - vkeys: [], // Cleared - bootstraps: requiredWitsLeft.bootstraps, - scripts: requiredWitsLeft.scripts, - plutusData: requiredWitsLeft.plutusData, - redeemers: requiredWitsLeft.redeemers, - scriptRefs: requiredWitsLeft.scriptRefs - }) - - const scriptHash = PlutusScriptWitness.hash(partialWitness.scriptWitness) - - // Remove the script hash - const filteredScripts = clearedRequiredWitsLeft.scripts.filter((h) => !ScriptHash.equals(h, scriptHash)) - const updatedRequiredWitsLeft = new RequiredWitnessSet({ - vkeys: clearedRequiredWitsLeft.vkeys, - bootstraps: clearedRequiredWitsLeft.bootstraps, - scripts: filteredScripts, - plutusData: clearedRequiredWitsLeft.plutusData, - redeemers: clearedRequiredWitsLeft.redeemers, - scriptRefs: clearedRequiredWitsLeft.scriptRefs - }) - - // Remove datum hash if provided - let finalRequiredWitsLeft = updatedRequiredWitsLeft - if (datum) { - const datumHash = hashPlutusData(datum) - const filteredPlutusData = updatedRequiredWitsLeft.plutusData.filter((h) => !DatumOption.equals(h, datumHash)) - finalRequiredWitsLeft = new RequiredWitnessSet({ - vkeys: updatedRequiredWitsLeft.vkeys, - bootstraps: updatedRequiredWitsLeft.bootstraps, - scripts: updatedRequiredWitsLeft.scripts, - plutusData: filteredPlutusData, - redeemers: updatedRequiredWitsLeft.redeemers, - scriptRefs: updatedRequiredWitsLeft.scriptRefs - }) - } - - if (finalRequiredWitsLeft.len() > 0) { - return yield* Eff.fail( - new InputBuilderError({ - message: `Missing the following witnesses for the input`, - cause: finalRequiredWitsLeft - }) - ) - } - - return { - input: this.input, - utxoInfo: this.utxoInfo, - aggregateWitness: InputAggregateWitnessData.plutusScript(partialWitness, requiredSigners, datum), - requiredWits - } - }.bind(this) - ) - } -} diff --git a/packages/evolution/src/builders/MintBuilder.ts b/packages/evolution/src/builders/MintBuilder.ts deleted file mode 100644 index 43787e58..00000000 --- a/packages/evolution/src/builders/MintBuilder.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { Data } from "effect" - -import type * as AssetName from "../core/AssetName.js" -import type * as KeyHash from "../core/KeyHash.js" -import type * as NativeScripts from "../core/NativeScripts.js" -import * as PolicyId from "../core/PolicyId.js" -import * as ScriptHash from "../core/ScriptHash.js" -import type { NativeScriptWitnessInfo, PartialPlutusWitness } from "./WitnessBuilder.js" -import { InputAggregateWitnessData, PlutusScriptWitness, RequiredWitnessSet } from "./WitnessBuilder.js" - -/** - * Error class for MintBuilder related operations. - * - * @since 2.0.0 - * @category errors - */ -export class MintBuilderError extends Data.TaggedError("MintBuilderError")<{ - message?: string - cause?: unknown -}> {} - -/** - * Convert ScriptHash to PolicyId - * Since both are based on Hash28, we can convert by extracting the hash - */ -function scriptHashToPolicyId(scriptHash: ScriptHash.ScriptHash): PolicyId.PolicyId { - return new PolicyId.PolicyId({ hash: scriptHash.hash }, { disableValidation: true }) -} - -/** - * Result of building a mint operation - * - * @since 2.0.0 - * @category model - */ -export interface MintBuilderResult { - policyId: PolicyId.PolicyId - assets: Map - aggregateWitness?: InputAggregateWitnessData - requiredWits: RequiredWitnessSet -} - -/** - * Builder for a single mint operation - * - * @since 2.0.0 - * @category builders - */ -export class SingleMintBuilder { - constructor(public readonly assets: Map) {} - - static new(assets: Map): SingleMintBuilder { - return new SingleMintBuilder(assets) - } - - static newSingleAsset(asset: AssetName.AssetName, amount: bigint): SingleMintBuilder { - const assets = new Map() - assets.set(asset, amount) - return new SingleMintBuilder(assets) - } - - nativeScript(nativeScript: NativeScripts.NativeScript, witnessInfo: NativeScriptWitnessInfo): MintBuilderResult { - const requiredWits = RequiredWitnessSet.default() - const scriptHash = ScriptHash.fromScript(nativeScript) - requiredWits.addScriptHash(scriptHash) - - return { - assets: this.assets, - policyId: scriptHashToPolicyId(scriptHash), - aggregateWitness: InputAggregateWitnessData.nativeScript(nativeScript, witnessInfo), - requiredWits - } - } - - plutusScript(partialWitness: PartialPlutusWitness, requiredSigners: Array): MintBuilderResult { - const requiredWits = RequiredWitnessSet.default() - - const scriptHash = PlutusScriptWitness.hash(partialWitness.scriptWitness) - requiredSigners.forEach((signer) => requiredWits.addVkeyKeyHash(signer)) - requiredWits.addScriptHash(scriptHash) - - return { - assets: this.assets, - policyId: scriptHashToPolicyId(scriptHash), - aggregateWitness: InputAggregateWitnessData.plutusScript( - partialWitness, - requiredSigners, - undefined // No datum for minting - ), - requiredWits - } - } -} diff --git a/packages/evolution/src/builders/OutputBuilder.ts b/packages/evolution/src/builders/OutputBuilder.ts deleted file mode 100644 index 133d5bd4..00000000 --- a/packages/evolution/src/builders/OutputBuilder.ts +++ /dev/null @@ -1,322 +0,0 @@ -import { Data, Effect as Eff, Schema } from "effect" - -import type * as AddressEras from "../core/AddressEras.js" -import * as Coin from "../core/Coin.js" -import * as PlutusData from "../core/Data.js" -import type * as DatumOption from "../core/DatumOption.js" -import type * as MultiAsset from "../core/MultiAsset.js" -import type * as ScriptRef from "../core/ScriptRef.js" -import * as TransactionOutput from "../core/TransactionOutput.js" -import * as Value from "../core/Value.js" -import { hashPlutusData } from "../utils/Hash.js" -import * as MinAda from "./utils/MinAda.js" - -/** - * Error class for OutputBuilder related operations. - * - * @since 2.0.0 - * @category errors - */ -export class OutputBuilderError extends Data.TaggedError("OutputBuilderError")<{ - message?: string - cause?: unknown -}> {} - -/** - * Result of building a single transaction output with optional communication datum. - * Communication datum is the full datum that gets included in witness while - * only its hash goes in the output itself. - * - * @since 2.0.0 - * @category model - */ -export class SingleOutputBuilderResult extends Schema.Class("SingleOutputBuilderResult")({ - output: TransactionOutput.TransactionOutput, - communicationDatum: Schema.optional(PlutusData.DataSchema) -}) { - /** - * Create a new SingleOutputBuilderResult with just an output. - * - * @since 2.0.0 - * @category constructors - */ - static new(output: TransactionOutput.TransactionOutput): SingleOutputBuilderResult { - return new SingleOutputBuilderResult({ - output, - communicationDatum: undefined - }) - } -} - -/** - * Builder for creating transaction outputs - first stage for setting address, datum, and script reference. - * This builder follows a two-stage pattern where basic fields are set first, then amount is set in the second stage. - * - * @since 2.0.0 - * @category builders - */ -export class TransactionOutputBuilder { - private address?: AddressEras.AddressEras - private datum?: DatumOption.DatumOption - private communicationDatum?: PlutusData.Data - private scriptRef?: ScriptRef.ScriptRef - - /** - * Create a new TransactionOutputBuilder. - * - * @since 2.0.0 - * @category constructors - */ - static new(): TransactionOutputBuilder { - return new TransactionOutputBuilder() - } - - /** - * Set the address for the transaction output. - * - * @since 2.0.0 - * @category setters - */ - withAddress(address: AddressEras.AddressEras): TransactionOutputBuilder { - this.address = address - return this - } - - /** - * Set a communication datum. This is a datum where the hash goes in the output - * but the full datum is included in the transaction witness. - * - * @since 2.0.0 - * @category setters - */ - withCommunicationData(datum: PlutusData.Data): TransactionOutputBuilder { - this.datum = hashPlutusData(datum) - this.communicationDatum = datum - return this - } - - /** - * Set the datum option directly (hash or inline datum). - * - * @since 2.0.0 - * @category setters - */ - withData(datum: DatumOption.DatumOption): TransactionOutputBuilder { - this.datum = datum - this.communicationDatum = undefined - return this - } - - /** - * Set the reference script for the transaction output. - * - * @since 2.0.0 - * @category setters - */ - withReferenceScript(scriptRef: ScriptRef.ScriptRef): TransactionOutputBuilder { - this.scriptRef = scriptRef - return this - } - - /** - * Move to the next stage of building where amount is set. - * - * @since 2.0.0 - * @category transitions - */ - next(): Eff.Effect { - if (!this.address) { - return Eff.fail( - new OutputBuilderError({ - message: "Address missing - call withAddress() before next()" - }) - ) - } - - return Eff.succeed( - new TransactionOutputAmountBuilder(this.address, this.datum, this.scriptRef, this.communicationDatum) - ) - } -} - -/** - * Builder for creating transaction outputs - second stage for setting the amount/value. - * This stage handles the more complex logic around minimum ADA requirements. - * - * @since 2.0.0 - * @category builders - */ -export class TransactionOutputAmountBuilder { - private amount?: Value.Value - - constructor( - private readonly address: AddressEras.AddressEras, - private readonly datum?: DatumOption.DatumOption, - private readonly scriptRef?: ScriptRef.ScriptRef, - private readonly communicationDatum?: PlutusData.Data - ) {} - - /** - * Set the value directly. Can be Coin or Value with assets. - * - * @since 2.0.0 - * @category setters - */ - withValue(amount: Value.Value): TransactionOutputAmountBuilder { - this.amount = amount - return this - } - - /** - * Set value from coin amount. - * - * @since 2.0.0 - * @category setters - */ - withCoin(coin: Coin.Coin): TransactionOutputAmountBuilder { - this.amount = Value.onlyCoin(coin) - return this - } - - /** - * Set the assets and calculate minimum required ADA automatically. - * This ensures the output meets the minimum ADA requirement based on the UTXO size. - * Based on CML Rust implementation algorithm. - * - * @since 2.0.0 - * @category setters - */ - withAssetAndMinRequiredCoin( - multiasset: MultiAsset.MultiAsset, - coinsPerUtxoByte: Coin.Coin - ): Eff.Effect { - return Eff.gen( - function* (this: TransactionOutputAmountBuilder) { - // Create a temporary output with zero ADA to get minimum possible size - const tempOutput = TransactionOutput.makeBabbage({ - address: this.address as any, // TODO: Fix address type validation - amount: Value.withAssets(Coin.make(0n), multiasset), - datumOption: this.datum, - scriptRef: this.scriptRef - }) - - // Calculate minimum possible coin requirement - const minPossibleCoin = yield* Eff.mapError( - MinAda.minAdaRequired(tempOutput, coinsPerUtxoByte), - (cause) => - new OutputBuilderError({ - message: "Failed to calculate minimum ADA requirement", - cause - }) - ) - - // Create test output with calculated minimum to double-check - const checkOutput = TransactionOutput.makeBabbage({ - address: this.address as any, - amount: Value.withAssets(minPossibleCoin, multiasset), - datumOption: this.datum, - scriptRef: this.scriptRef - }) - - // Recalculate to ensure accuracy (matches Rust implementation) - const requiredCoin = yield* Eff.mapError( - MinAda.minAdaRequired(checkOutput, coinsPerUtxoByte), - (cause) => - new OutputBuilderError({ - message: "Failed to recalculate minimum ADA requirement", - cause - }) - ) - - // Set the final value with the correctly calculated minimum ADA - this.amount = Value.withAssets(requiredCoin, multiasset) - return this - }.bind(this) - ) - } - - /** - * Build the final transaction output result. - * - * @since 2.0.0 - * @category builders - */ - build(): Eff.Effect { - if (!this.amount) { - return Eff.fail( - new OutputBuilderError({ - message: "Amount missing - call withValue(), withCoin(), or withAssetAndMinRequiredCoin() before build()" - }) - ) - } - - // Use BabbageTransactionOutput for full feature support - // Note: In real implementation, should validate address type first - const output = TransactionOutput.makeBabbage({ - address: this.address as any, // TODO: Add proper address type validation - amount: this.amount, - datumOption: this.datum, - scriptRef: this.scriptRef - }) - - return Eff.succeed( - new SingleOutputBuilderResult({ - output, - communicationDatum: this.communicationDatum - }) - ) - } -} - -// ============================================================================ -// Effect Namespace - Effect-based Error Handling -// ============================================================================ - -/** - * Effect-based error handling variants for functions that can fail. - * Returns Effect for composable error handling. - * - * @since 2.0.0 - * @category effect - */ -export namespace OutputBuilderEffect { - /** - * Create a new TransactionOutputBuilder using Effect error handling. - * - * @since 2.0.0 - * @category constructors - */ - export const newOutputBuilder = (): Eff.Effect => - Eff.succeed(TransactionOutputBuilder.new()) - - /** - * Create a SingleOutputBuilderResult from just an output using Effect error handling. - * - * @since 2.0.0 - * @category constructors - */ - export const newSingleResult = ( - output: TransactionOutput.TransactionOutput - ): Eff.Effect => Eff.succeed(SingleOutputBuilderResult.new(output)) -} - -// ============================================================================ -// Root Namespace Functions (Sync API) -// ============================================================================ - -/** - * Create a new TransactionOutputBuilder. - * - * @since 2.0.0 - * @category constructors - */ -export const newOutputBuilder = (): TransactionOutputBuilder => TransactionOutputBuilder.new() - -/** - * Create a SingleOutputBuilderResult from just an output. - * - * @since 2.0.0 - * @category constructors - */ -export const newSingleResult = (output: TransactionOutput.TransactionOutput): SingleOutputBuilderResult => - SingleOutputBuilderResult.new(output) diff --git a/packages/evolution/src/builders/ProposalBuilder.ts b/packages/evolution/src/builders/ProposalBuilder.ts deleted file mode 100644 index 22025ce7..00000000 --- a/packages/evolution/src/builders/ProposalBuilder.ts +++ /dev/null @@ -1,239 +0,0 @@ -import { Data, Effect as Eff } from "effect" - -import type * as PlutusData from "../core/Data.js" -import * as DatumOption from "../core/DatumOption.js" -import type * as KeyHash from "../core/KeyHash.js" -import type * as NativeScripts from "../core/NativeScripts.js" -import type * as ProposalProcedure from "../core/ProposalProcedure.js" -import * as ScriptHash from "../core/ScriptHash.js" -import { hashPlutusData } from "../utils/Hash.js" -import type { NativeScriptWitnessInfo, PartialPlutusWitness } from "./WitnessBuilder.js" -import { InputAggregateWitnessData, PlutusScriptWitness, RequiredWitnessSet } from "./WitnessBuilder.js" - -/** - * Error class for ProposalBuilder related operations. - * - * @since 2.0.0 - * @category errors - */ -export class ProposalBuilderError extends Data.TaggedError("ProposalBuilderError")<{ - message?: string - cause?: unknown -}> {} - -/** - * Result of building proposals - * - * @since 2.0.0 - * @category model - */ -export interface ProposalBuilderResult { - proposals: Array - requiredWits: RequiredWitnessSet - aggregateWitnesses: Array -} - -/** - * Builder for governance proposals - * - * @since 2.0.0 - * @category builders - */ -export class ProposalBuilder { - private result: ProposalBuilderResult - - constructor() { - this.result = { - proposals: [], - requiredWits: RequiredWitnessSet.default(), - aggregateWitnesses: [] - } - } - - static new(): ProposalBuilder { - return new ProposalBuilder() - } - - withProposal(proposal: ProposalProcedure.ProposalProcedure): Eff.Effect { - return Eff.gen( - function* (this: ProposalBuilder) { - // Check if proposal uses script hash - const scriptHash = getProposalScriptHash(proposal) - if (scriptHash) { - return yield* Eff.fail( - new ProposalBuilderError({ - message: "Proposal uses script. Call withPlutusProposal() instead." - }) - ) - } - - this.result.proposals.push(proposal) - return this - }.bind(this) - ) - } - - withNativeScriptProposal( - proposal: ProposalProcedure.ProposalProcedure, - nativeScript: NativeScripts.NativeScript, - witnessInfo: NativeScriptWitnessInfo - ): Eff.Effect { - return Eff.gen( - function* (this: ProposalBuilder) { - const proposalScriptHash = getProposalScriptHash(proposal) - const scriptHash = ScriptHash.fromScript(nativeScript) - - if (!proposalScriptHash) { - return yield* Eff.fail( - new ProposalBuilderError({ - message: "Proposal uses key hash. Call withProposal() instead." - }) - ) - } - - if (!ScriptHash.equals(proposalScriptHash, scriptHash)) { - const errRequiredWits = RequiredWitnessSet.default() - errRequiredWits.addScriptHash(proposalScriptHash) - return yield* Eff.fail( - new ProposalBuilderError({ - message: "Missing the following witnesses for the proposal", - cause: errRequiredWits - }) - ) - } - - this.result.requiredWits.addScriptHash(proposalScriptHash) - this.result.proposals.push(proposal) - this.result.aggregateWitnesses.push(InputAggregateWitnessData.nativeScript(nativeScript, witnessInfo)) - - return this - }.bind(this) - ) - } - - withPlutusProposal( - proposal: ProposalProcedure.ProposalProcedure, - partialWitness: PartialPlutusWitness, - requiredSigners: Array, - datum: PlutusData.Data - ): Eff.Effect { - return this.withPlutusProposalImpl(proposal, partialWitness, requiredSigners, datum) - } - - withPlutusProposalInlineDatum( - proposal: ProposalProcedure.ProposalProcedure, - partialWitness: PartialPlutusWitness, - requiredSigners: Array - ): Eff.Effect { - return this.withPlutusProposalImpl(proposal, partialWitness, requiredSigners, undefined) - } - - private withPlutusProposalImpl( - proposal: ProposalProcedure.ProposalProcedure, - partialWitness: PartialPlutusWitness, - requiredSigners: Array, - datum?: PlutusData.Data - ): Eff.Effect { - return Eff.gen( - function* (this: ProposalBuilder) { - const requiredWits = RequiredWitnessSet.default() - requiredSigners.forEach((signer) => requiredWits.addVkeyKeyHash(signer)) - - const proposalScriptHash = getProposalScriptHash(proposal) - if (!proposalScriptHash) { - return yield* Eff.fail( - new ProposalBuilderError({ - message: "Proposal uses key hash. Call withProposal() instead." - }) - ) - } - - requiredWits.addScriptHash(proposalScriptHash) - const requiredWitsLeft = structuredClone(requiredWits) - - // Clear vkeys as we don't know which ones will be used - const clearedRequiredWitsLeft = new RequiredWitnessSet({ - vkeys: [], // Cleared - bootstraps: requiredWitsLeft.bootstraps, - scripts: requiredWitsLeft.scripts, - plutusData: requiredWitsLeft.plutusData, - redeemers: requiredWitsLeft.redeemers, - scriptRefs: requiredWitsLeft.scriptRefs - }) - - const scriptHash = PlutusScriptWitness.hash(partialWitness.scriptWitness) - - // Remove the script hash - const filteredScripts = clearedRequiredWitsLeft.scripts.filter((h) => !ScriptHash.equals(h, scriptHash)) - const updatedRequiredWitsLeft = new RequiredWitnessSet({ - vkeys: clearedRequiredWitsLeft.vkeys, - bootstraps: clearedRequiredWitsLeft.bootstraps, - scripts: filteredScripts, - plutusData: clearedRequiredWitsLeft.plutusData, - redeemers: clearedRequiredWitsLeft.redeemers, - scriptRefs: clearedRequiredWitsLeft.scriptRefs - }) - - // Remove datum hash if provided - let finalRequiredWitsLeft = updatedRequiredWitsLeft - if (datum) { - const datumHash = hashPlutusData(datum) - const filteredPlutusData = updatedRequiredWitsLeft.plutusData.filter((h) => !DatumOption.equals(h, datumHash)) - finalRequiredWitsLeft = new RequiredWitnessSet({ - vkeys: updatedRequiredWitsLeft.vkeys, - bootstraps: updatedRequiredWitsLeft.bootstraps, - scripts: updatedRequiredWitsLeft.scripts, - plutusData: filteredPlutusData, - redeemers: updatedRequiredWitsLeft.redeemers, - scriptRefs: updatedRequiredWitsLeft.scriptRefs - }) - } - - if (finalRequiredWitsLeft.len() > 0) { - return yield* Eff.fail( - new ProposalBuilderError({ - message: "Missing the following witnesses for the proposal", - cause: finalRequiredWitsLeft - }) - ) - } - - this.result.proposals.push(proposal) - this.result.requiredWits.addAll(requiredWits) - this.result.aggregateWitnesses.push( - InputAggregateWitnessData.plutusScript(partialWitness, requiredSigners, datum) - ) - - return this - }.bind(this) - ) - } - - build(): ProposalBuilderResult { - return this.result - } -} - -/** - * Helper function to get script hash from a proposal - * Returns undefined if proposal uses key hash - * Based on Conway CDDL: only ParameterChangeAction and TreasuryWithdrawalsAction have policy_hash - */ -function getProposalScriptHash(proposal: ProposalProcedure.ProposalProcedure): ScriptHash.ScriptHash | undefined { - const action = proposal.governanceAction - - switch (action._tag) { - case "ParameterChangeAction": - return action.policyHash || undefined - case "TreasuryWithdrawalsAction": - return action.policyHash || undefined - case "HardForkInitiationAction": - case "NoConfidenceAction": - case "UpdateCommitteeAction": - case "NewConstitutionAction": - case "InfoAction": - return undefined - default: - return undefined - } -} diff --git a/packages/evolution/src/builders/RedeemerBuilder.ts b/packages/evolution/src/builders/RedeemerBuilder.ts deleted file mode 100644 index b2103f14..00000000 --- a/packages/evolution/src/builders/RedeemerBuilder.ts +++ /dev/null @@ -1,432 +0,0 @@ -import { Data, Effect as Eff, Schema } from "effect" - -import * as PlutusData from "../core/Data.js" -import type * as PolicyId from "../core/PolicyId.js" -import * as Redeemer from "../core/Redeemer.js" -import type * as RewardAddress from "../core/RewardAddress.js" -import type * as TransactionInput from "../core/TransactionInput.js" -import type { RedeemerWitnessKey } from "./WitnessBuilder.js" - -/** - * Error class for missing execution units. - * - * @since 2.0.0 - * @category errors - */ -export class MissingExunitError extends Data.TaggedError("MissingExunitError")<{ - message?: string - tag: Redeemer.RedeemerTag - index: number - key: string -}> {} - -/** - * Error class for RedeemerBuilder related operations. - * - * @since 2.0.0 - * @category errors - */ -export class RedeemerBuilderError extends Data.TaggedError("RedeemerBuilderError")<{ - message?: string - cause?: unknown -}> {} - -/** - * Redeemer without the tag or index for builder code to return partial redeemers. - * - * @since 2.0.0 - * @category model - */ -export class UntaggedRedeemer extends Schema.Class("UntaggedRedeemer")({ - data: PlutusData.DataSchema, - exUnits: Redeemer.ExUnits -}) { - static new(data: PlutusData.Data, exUnits: Redeemer.ExUnits): UntaggedRedeemer { - return new UntaggedRedeemer({ data, exUnits }) - } -} - -/** - * Union type for untagged redeemer placeholders. - * - * @since 2.0.0 - * @category model - */ -export const UntaggedRedeemerPlaceholder = Schema.Union( - Schema.Struct({ - _tag: Schema.Literal("JustData"), - data: PlutusData.DataSchema - }), - Schema.Struct({ - _tag: Schema.Literal("Full"), - redeemer: UntaggedRedeemer - }) -).annotations({ - identifier: "UntaggedRedeemerPlaceholder", - description: "Placeholder for redeemer data that may be partial or complete" -}) - -export type UntaggedRedeemerPlaceholder = typeof UntaggedRedeemerPlaceholder.Type - -/** - * Helper function to extract data from an untagged redeemer placeholder. - * - * @since 2.0.0 - * @category utilities - */ -export const getPlaceholderData = (placeholder: UntaggedRedeemerPlaceholder): PlutusData.Data => { - switch (placeholder._tag) { - case "JustData": - return placeholder.data - case "Full": - return placeholder.redeemer.data - } -} - -/** - * Builder for creating redeemer sets. - * - * In order to calculate the index from the sorted set, "add*" methods in this builder - * must be called along with the "add*" methods in transaction builder. - * - * @since 2.0.0 - * @category builders - */ -export class RedeemerSetBuilder { - private spend: Map = new Map() - private mint: Map = new Map() - private reward: Map = new Map() - private cert: Array = [] - private proposals: Array = [] - private votes: Array = [] - - /** - * Create a new RedeemerSetBuilder instance. - * - * @since 2.0.0 - * @category constructors - */ - static new(): RedeemerSetBuilder { - return new RedeemerSetBuilder() - } - - /** - * Check if the builder is empty (no redeemers tracked). - * - * @since 2.0.0 - * @category utilities - */ - isEmpty(): boolean { - return ( - this.spend.size === 0 && - this.mint.size === 0 && - this.reward.size === 0 && - this.cert.length === 0 && - this.proposals.length === 0 && - this.votes.length === 0 - ) - } - - /** - * Update execution units for a specific redeemer. - * Will override existing value if called twice with the same key. - * - * @since 2.0.0 - * @category updates - */ - updateExUnits(key: RedeemerWitnessKey, exUnits: Redeemer.ExUnits): Eff.Effect { - const index = Number(key.index) - - switch (key.tag) { - case "spend": { - const entries = Array.from(this.spend.entries()).sort((a, b) => a[0].localeCompare(b[0])) - if (index >= entries.length) { - return Eff.fail( - new RedeemerBuilderError({ - message: `Spend index ${index} out of bounds`, - cause: new Error(`Only ${entries.length} spend entries available`) - }) - ) - } - const [inputKey, placeholder] = entries[index] - if (!placeholder) { - return Eff.fail( - new RedeemerBuilderError({ - message: "Cannot update ex units for null placeholder" - }) - ) - } - const data = getPlaceholderData(placeholder) - this.spend.set(inputKey, { - _tag: "Full", - redeemer: UntaggedRedeemer.new(data, exUnits) - }) - return Eff.succeed(undefined) - } - case "mint": { - const entries = Array.from(this.mint.entries()).sort((a, b) => a[0].localeCompare(b[0])) - if (index >= entries.length) { - return Eff.fail( - new RedeemerBuilderError({ - message: `Mint index ${index} out of bounds`, - cause: new Error(`Only ${entries.length} mint entries available`) - }) - ) - } - const [policyKey, placeholder] = entries[index] - if (!placeholder) { - return Eff.fail( - new RedeemerBuilderError({ - message: "Cannot update ex units for null placeholder" - }) - ) - } - const data = getPlaceholderData(placeholder) - this.mint.set(policyKey, { - _tag: "Full", - redeemer: UntaggedRedeemer.new(data, exUnits) - }) - return Eff.succeed(undefined) - } - case "reward": { - const entries = Array.from(this.reward.entries()).sort((a, b) => a[0].localeCompare(b[0])) - if (index >= entries.length) { - return Eff.fail( - new RedeemerBuilderError({ - message: `Reward index ${index} out of bounds`, - cause: new Error(`Only ${entries.length} reward entries available`) - }) - ) - } - const [rewardKey, placeholder] = entries[index] - if (!placeholder) { - return Eff.fail( - new RedeemerBuilderError({ - message: "Cannot update ex units for null placeholder" - }) - ) - } - const data = getPlaceholderData(placeholder) - this.reward.set(rewardKey, { - _tag: "Full", - redeemer: UntaggedRedeemer.new(data, exUnits) - }) - return Eff.succeed(undefined) - } - case "cert": { - if (index >= this.cert.length) { - return Eff.fail( - new RedeemerBuilderError({ - message: `Cert index ${index} out of bounds`, - cause: new Error(`Only ${this.cert.length} cert entries available`) - }) - ) - } - const placeholder = this.cert[index] - if (!placeholder) { - return Eff.fail( - new RedeemerBuilderError({ - message: "Cannot update ex units for null placeholder" - }) - ) - } - const data = getPlaceholderData(placeholder) - this.cert[index] = { - _tag: "Full", - redeemer: UntaggedRedeemer.new(data, exUnits) - } - return Eff.succeed(undefined) - } - } - } - - /** - * Add a spend input result to the builder. - * - * @since 2.0.0 - * @category adds - */ - addSpend(input: TransactionInput.TransactionInput, redeemerData?: PlutusData.Data): void { - const key = JSON.stringify(input) - if (redeemerData) { - this.spend.set(key, { _tag: "JustData", data: redeemerData }) - } else { - this.spend.set(key, null) - } - } - - /** - * Add a mint result to the builder. - * - * @since 2.0.0 - * @category adds - */ - addMint(policyId: PolicyId.PolicyId, redeemerData?: PlutusData.Data): void { - const key = JSON.stringify(policyId) - if (redeemerData) { - this.mint.set(key, { _tag: "JustData", data: redeemerData }) - } else { - this.mint.set(key, null) - } - } - - /** - * Add a reward withdrawal result to the builder. - * - * @since 2.0.0 - * @category adds - */ - addReward(address: RewardAddress.RewardAddress, redeemerData?: PlutusData.Data): void { - const key = JSON.stringify(address) - if (redeemerData) { - this.reward.set(key, { _tag: "JustData", data: redeemerData }) - } else { - this.reward.set(key, null) - } - } - - /** - * Add a certificate result to the builder. - * - * @since 2.0.0 - * @category adds - */ - addCert(redeemerData?: PlutusData.Data): void { - if (redeemerData) { - this.cert.push({ _tag: "JustData", data: redeemerData }) - } else { - this.cert.push(null) - } - } - - /** - * Add proposal results to the builder. - * - * @since 2.0.0 - * @category adds - */ - addProposal(redeemerData?: PlutusData.Data): void { - if (redeemerData) { - this.proposals.push({ _tag: "JustData", data: redeemerData }) - } else { - this.proposals.push(null) - } - } - - /** - * Add vote results to the builder. - * - * @since 2.0.0 - * @category adds - */ - addVote(redeemerData?: PlutusData.Data): void { - if (redeemerData) { - this.votes.push({ _tag: "JustData", data: redeemerData }) - } else { - this.votes.push(null) - } - } - - /** - * Build the final redeemers array. - * - * @since 2.0.0 - * @category builders - */ - build(defaultToDummyExunits: boolean = false): Eff.Effect, RedeemerBuilderError> { - const redeemers: Array = [] - - const spendEntries = Array.from(this.spend.entries()).sort((a, b) => a[0].localeCompare(b[0])) - const mintEntries = Array.from(this.mint.entries()).sort((a, b) => a[0].localeCompare(b[0])) - const rewardEntries = Array.from(this.reward.entries()).sort((a, b) => a[0].localeCompare(b[0])) - const certEntries = this.cert.map( - (entry: UntaggedRedeemerPlaceholder | null, i: number) => - [`${i}`, entry] as [string, UntaggedRedeemerPlaceholder | null] - ) - - return Eff.Do.pipe( - Eff.tap(() => this.removePlaceholdersAndTag(redeemers, "spend", spendEntries, defaultToDummyExunits)), - Eff.tap(() => this.removePlaceholdersAndTag(redeemers, "mint", mintEntries, defaultToDummyExunits)), - Eff.tap(() => this.removePlaceholdersAndTag(redeemers, "reward", rewardEntries, defaultToDummyExunits)), - Eff.tap(() => this.removePlaceholdersAndTag(redeemers, "cert", certEntries, defaultToDummyExunits)), - Eff.map(() => redeemers) - ) - } - - private removePlaceholdersAndTag( - redeemers: Array, - tag: Redeemer.RedeemerTag, - entries: Array<[string, UntaggedRedeemerPlaceholder | null]>, - defaultToDummyExunits: boolean - ): Eff.Effect { - try { - const results: Array = [] - - for (let i = 0; i < entries.length; i++) { - const [key, placeholder] = entries[i] - - if (!placeholder) { - results.push(null) - continue - } - - switch (placeholder._tag) { - case "JustData": - if (!defaultToDummyExunits) { - return Eff.fail( - new RedeemerBuilderError({ - message: "Missing execution units", - cause: new MissingExunitError({ - message: `Missing exunit for ${tag} with key ${key} and index ${i}`, - tag, - index: i, - key - }) - }) - ) - } else { - results.push(UntaggedRedeemer.new(placeholder.data, [BigInt(0), BigInt(0)])) - } - break - case "Full": - results.push(placeholder.redeemer) - break - } - } - - const taggedRedeemers = this.tagRedeemers(tag, results) - redeemers.push(...taggedRedeemers) - return Eff.succeed(undefined) - } catch (error) { - return Eff.fail( - new RedeemerBuilderError({ - message: `Failed to process ${tag} redeemers`, - cause: error - }) - ) - } - } - - private tagRedeemers( - tag: Redeemer.RedeemerTag, - untaggedRedeemers: Array - ): Array { - const results: Array = [] - - for (let index = 0; index < untaggedRedeemers.length; index++) { - const untagged = untaggedRedeemers[index] - if (untagged) { - results.push( - new Redeemer.Redeemer({ - tag, - index: BigInt(index), - data: untagged.data, - exUnits: untagged.exUnits - }) - ) - } - } - - return results - } -} diff --git a/packages/evolution/src/builders/TxBuilder.ts b/packages/evolution/src/builders/TxBuilder.ts deleted file mode 100644 index eff91e18..00000000 --- a/packages/evolution/src/builders/TxBuilder.ts +++ /dev/null @@ -1,986 +0,0 @@ -import { Data, Effect as Eff, Schema } from "effect" -import type { NonEmptyArray } from "effect/Array" - -import type * as AddressEras from "../core/AddressEras.js" -import type * as AuxiliaryData from "../core/AuxiliaryData.js" -import * as Coin from "../core/Coin.js" -import * as KeyHash from "../core/KeyHash.js" -import * as Mint from "../core/Mint.js" -import type * as NetworkId from "../core/NetworkId.js" -import * as NonZeroInt64 from "../core/NonZeroInt64.js" -import * as Transaction from "../core/Transaction.js" -import * as TransactionBody from "../core/TransactionBody.js" -import * as TransactionInput from "../core/TransactionInput.js" -import * as TransactionOutput from "../core/TransactionOutput.js" -import * as TransactionWitnessSet from "../core/TransactionWitnessSet.js" -import * as Value from "../core/Value.js" -import * as Withdrawals from "../core/Withdrawals.js" -import * as Hash from "../utils/Hash.js" -import type { CertificateBuilderResult } from "./CertificateBuilder.js" -import type { InputBuilderResult } from "./InputBuilder.js" -import type { MintBuilderResult } from "./MintBuilder.js" -import { type SingleOutputBuilderResult } from "./OutputBuilder.js" -import type { ProposalBuilderResult } from "./ProposalBuilder.js" -import type { VoteBuilderResult } from "./VoteBuilder.js" -import type { WithdrawalBuilderResult } from "./WithdrawalBuilder.js" - -/** - * Error class for TxBuilder related operations. - * - * @since 2.0.0 - * @category errors - */ -export class TxBuilderError extends Data.TaggedError("TxBuilderError")<{ - message?: string - cause?: unknown -}> {} - -/** - * Configuration error for missing transaction builder parameters. - * - * @since 2.0.0 - * @category errors - */ -export class TxBuilderConfigError extends Data.TaggedError("TxBuilderConfigError")<{ - message?: string - missingFields?: Array -}> {} - -/** - * UTXO structure for transaction inputs. - * This matches the CIP30 interface and is useful for builders. - * - * @since 2.0.0 - * @category model - */ -export class TransactionUnspentOutput extends Schema.Class("TransactionUnspentOutput")({ - input: TransactionInput.TransactionInput, - output: TransactionOutput.TransactionOutput -}) { - /** - * Create a new TransactionUnspentOutput. - * - * @since 2.0.0 - * @category constructors - */ - static new( - input: TransactionInput.TransactionInput, - output: TransactionOutput.TransactionOutput - ): TransactionUnspentOutput { - return new TransactionUnspentOutput({ input, output }) - } -} - -/** - * Coin selection strategy based on CIP-2 standard. - * - * @since 2.0.0 - * @category model - */ -export const CoinSelectionStrategyCIP2 = Schema.Literal( - "LargestFirst", - "RandomImprove", - "RandomImproveMultiAsset" -).annotations({ - identifier: "TxBuilder.CoinSelectionStrategyCIP2", - description: "Coin selection algorithms implementing CIP-2" -}) - -export type CoinSelectionStrategyCIP2 = typeof CoinSelectionStrategyCIP2.Type - -/** - * Change selection algorithm for creating change outputs. - * - * @since 2.0.0 - * @category model - */ -export const ChangeSelectionAlgo = Schema.Literal("Default").annotations({ - identifier: "TxBuilder.ChangeSelectionAlgo", - description: "Algorithm for creating transaction change outputs" -}) - -export type ChangeSelectionAlgo = typeof ChangeSelectionAlgo.Type - -/** - * Linear fee algorithm configuration. - * - * @since 2.0.0 - * @category model - */ -export class LinearFee extends Schema.Class("LinearFee")({ - constant: Coin.Coin.annotations({ - description: "Base fee constant in lovelace" - }), - coefficient: Coin.Coin.annotations({ - description: "Fee coefficient per byte in lovelace" - }) -}) {} - -/** - * Ex-unit prices for script execution. - * - * @since 2.0.0 - * @category model - */ -export class ExUnitPrices extends Schema.Class("ExUnitPrices")({ - memPrice: Schema.Struct({ - numerator: Schema.BigInt, - denominator: Schema.BigInt - }), - stepPrice: Schema.Struct({ - numerator: Schema.BigInt, - denominator: Schema.BigInt - }) -}) {} - -/** - * Transaction builder configuration with protocol parameters. - * - * @since 2.0.0 - * @category model - */ -export class TransactionBuilderConfig extends Schema.Class("TransactionBuilderConfig")({ - feeAlgo: LinearFee, - coinsPerUtxoByte: Coin.Coin, - poolDeposit: Coin.Coin, - keyDeposit: Coin.Coin, - maxValueSize: Schema.Number, - maxTxSize: Schema.Number, - utxoCostPerWord: Schema.optional(Coin.Coin), - exUnitPrices: Schema.optional(ExUnitPrices), - preferPureChange: Schema.optional(Schema.Boolean) -}) {} - -/** - * Builder for creating TransactionBuilderConfig with validation. - * - * @since 2.0.0 - * @category builders - */ -export class TransactionBuilderConfigBuilder { - private feeAlgo?: LinearFee - private coinsPerUtxoByte?: Coin.Coin - private poolDeposit?: Coin.Coin - private keyDeposit?: Coin.Coin - private maxValueSize?: number - private maxTxSize?: number - private utxoCostPerWord?: Coin.Coin - private exUnitPrices?: ExUnitPrices - private preferPureChange?: boolean - - /** - * Create a new TransactionBuilderConfigBuilder. - * - * @since 2.0.0 - * @category constructors - */ - static new(): TransactionBuilderConfigBuilder { - return new TransactionBuilderConfigBuilder() - } - - /** - * Set the fee algorithm. - * - * @since 2.0.0 - * @category setters - */ - feeAlgorithm(feeAlgo: LinearFee): TransactionBuilderConfigBuilder { - this.feeAlgo = feeAlgo - return this - } - - /** - * Set coins per UTXO byte for minimum ADA calculation. - * - * @since 2.0.0 - * @category setters - */ - coinsPerUtxoWord(coins: Coin.Coin): TransactionBuilderConfigBuilder { - this.coinsPerUtxoByte = coins - return this - } - - /** - * Set pool registration deposit. - * - * @since 2.0.0 - * @category setters - */ - poolDepositAmount(deposit: Coin.Coin): TransactionBuilderConfigBuilder { - this.poolDeposit = deposit - return this - } - - /** - * Set key registration deposit. - * - * @since 2.0.0 - * @category setters - */ - keyDepositAmount(deposit: Coin.Coin): TransactionBuilderConfigBuilder { - this.keyDeposit = deposit - return this - } - - /** - * Set maximum value size per output. - * - * @since 2.0.0 - * @category setters - */ - maxValueSizeLimit(size: number): TransactionBuilderConfigBuilder { - this.maxValueSize = size - return this - } - - /** - * Set maximum transaction size. - * - * @since 2.0.0 - * @category setters - */ - maxTxSizeLimit(size: number): TransactionBuilderConfigBuilder { - this.maxTxSize = size - return this - } - - /** - * Set UTXO cost per word (legacy parameter). - * - * @since 2.0.0 - * @category setters - */ - utxoCostPerWordAmount(cost: Coin.Coin): TransactionBuilderConfigBuilder { - this.utxoCostPerWord = cost - return this - } - - /** - * Set execution unit prices for script fees. - * - * @since 2.0.0 - * @category setters - */ - executionUnitPrices(prices: ExUnitPrices): TransactionBuilderConfigBuilder { - this.exUnitPrices = prices - return this - } - - /** - * Set preference for pure change (no assets). - * - * @since 2.0.0 - * @category setters - */ - preferPureChangeOutput(prefer: boolean): TransactionBuilderConfigBuilder { - this.preferPureChange = prefer - return this - } - - /** - * Build the configuration with validation. - * - * @since 2.0.0 - * @category builders - */ - build(): Eff.Effect { - const missingFields: Array = [] - - if (!this.feeAlgo) missingFields.push("feeAlgo") - if (!this.coinsPerUtxoByte) missingFields.push("coinsPerUtxoByte") - if (!this.poolDeposit) missingFields.push("poolDeposit") - if (!this.keyDeposit) missingFields.push("keyDeposit") - if (this.maxValueSize === undefined) missingFields.push("maxValueSize") - if (this.maxTxSize === undefined) missingFields.push("maxTxSize") - - if (missingFields.length > 0) { - return Eff.fail( - new TxBuilderConfigError({ - message: `Missing required configuration fields: ${missingFields.join(", ")}`, - missingFields - }) - ) - } - - return Eff.succeed( - new TransactionBuilderConfig({ - feeAlgo: this.feeAlgo!, - coinsPerUtxoByte: this.coinsPerUtxoByte!, - poolDeposit: this.poolDeposit!, - keyDeposit: this.keyDeposit!, - maxValueSize: this.maxValueSize!, - maxTxSize: this.maxTxSize!, - utxoCostPerWord: this.utxoCostPerWord, - exUnitPrices: this.exUnitPrices, - preferPureChange: this.preferPureChange - }) - ) - } -} - -/** - * Result of building a signed transaction with body and witness set. - * - * @since 2.0.0 - * @category model - */ -export class SignedTxBuilder extends Schema.Class("SignedTxBuilder")({ - body: TransactionBody.TransactionBody, - witnessSet: TransactionWitnessSet.TransactionWitnessSet, - auxiliaryData: Schema.optional(Schema.Any) // AuxiliaryData when available -}) { - /** - * Build the final transaction. - * - * @since 2.0.0 - * @category builders - */ - build(): Transaction.Transaction { - return new Transaction.Transaction({ - body: this.body, - witnessSet: this.witnessSet, - isValid: true, - auxiliaryData: this.auxiliaryData - }) - } -} - -/** - * Main transaction builder for constructing Cardano transactions. - * Handles inputs, outputs, certificates, withdrawals, minting, fees, and witness requirements. - * - * @since 2.0.0 - * @category builders - */ -export class TransactionBuilder { - private inputs: Array = [] - private outputs: Array = [] - private utxos: Array = [] - private referenceInputs: Array = [] - private certificates: Array = [] - private withdrawals: Array = [] - private mints: Array = [] - private proposals: Array = [] - private votes: Array = [] - private collateral: Array = [] - private requiredSigners: Set = new Set() // Use hex representation for deduplication - private fee?: Coin.Coin - private ttl?: bigint - private validityStart?: bigint - private auxiliaryData?: AuxiliaryData.AuxiliaryData - private networkId?: NetworkId.NetworkId - - constructor(private readonly config: TransactionBuilderConfig) {} - - /** - * Create a new TransactionBuilder with configuration. - * - * @since 2.0.0 - * @category constructors - */ - static new(config: TransactionBuilderConfig): TransactionBuilder { - return new TransactionBuilder(config) - } - - // ============================================================================ - // Input/Output Management - // ============================================================================ - - /** - * Add a transaction input with witness requirements. - * - * @since 2.0.0 - * @category inputs - */ - addInput(result: InputBuilderResult): Eff.Effect { - this.inputs.push(result) - return Eff.succeed(undefined) - } - - /** - * Add a UTXO for coin selection. - * - * @since 2.0.0 - * @category inputs - */ - addUtxo(result: InputBuilderResult): void { - this.utxos.push(result) - } - - /** - * Add a reference input (read-only). - * - * @since 2.0.0 - * @category inputs - */ - addReferenceInput(utxo: TransactionUnspentOutput): void { - this.referenceInputs.push(utxo) - } - - /** - * Add a transaction output. - * - * @since 2.0.0 - * @category outputs - */ - addOutput(result: SingleOutputBuilderResult): Eff.Effect { - // Validate output size doesn't exceed max value size - if (this.getOutputSize(result) > this.config.maxValueSize) { - return Eff.fail( - new TxBuilderError({ - message: `Output exceeds max value size of ${this.config.maxValueSize} bytes` - }) - ) - } - this.outputs.push(result) - return Eff.succeed(undefined) - } - - // ============================================================================ - // Transaction Components - // ============================================================================ - - /** - * Add a certificate. - * - * @since 2.0.0 - * @category components - */ - addCert(result: CertificateBuilderResult): void { - this.certificates.push(result) - } - - /** - * Add a withdrawal. - * - * @since 2.0.0 - * @category components - */ - addWithdrawal(result: WithdrawalBuilderResult): void { - this.withdrawals.push(result) - } - - /** - * Add a mint operation. - * - * @since 2.0.0 - * @category components - */ - addMint(result: MintBuilderResult): Eff.Effect { - this.mints.push(result) - return Eff.succeed(undefined) - } - - /** - * Add a governance proposal. - * - * @since 2.0.0 - * @category governance - */ - addProposal(result: ProposalBuilderResult): void { - this.proposals.push(result) - } - - /** - * Add a governance vote. - * - * @since 2.0.0 - * @category governance - */ - addVote(result: VoteBuilderResult): void { - this.votes.push(result) - } - - /** - * Add a collateral input. - * - * @since 2.0.0 - * @category collateral - */ - addCollateral(result: InputBuilderResult): Eff.Effect { - this.collateral.push(result) - return Eff.succeed(undefined) - } - - /** - * Add auxiliary data (metadata). - * - * @since 2.0.0 - * @category metadata - */ - addAuxiliaryData(auxData: AuxiliaryData.AuxiliaryData): void { - this.auxiliaryData = auxData - } - - /** - * Add a required signer. - * - * @since 2.0.0 - * @category signers - */ - addRequiredSigner(keyHash: KeyHash.KeyHash): void { - this.requiredSigners.add(KeyHash.toHex(keyHash)) - } - - // ============================================================================ - // Fee and Time Management - // ============================================================================ - - /** - * Set the transaction fee explicitly. - * - * @since 2.0.0 - * @category fees - */ - setFee(fee: Coin.Coin): void { - this.fee = fee - } - - /** - * Set the time-to-live (TTL) for the transaction. - * - * @since 2.0.0 - * @category time - */ - setTtl(ttl: bigint): void { - this.ttl = ttl - } - - /** - * Set the validity start interval. - * - * @since 2.0.0 - * @category time - */ - setValidityStartInterval(start: bigint): void { - this.validityStart = start - } - - /** - * Set the network ID. - * - * @since 2.0.0 - * @category network - */ - setNetworkId(networkId: NetworkId.NetworkId): void { - this.networkId = networkId - } - - // ============================================================================ - // Coin Selection - // ============================================================================ - - /** - * Select UTXOs using the specified coin selection strategy. - * - * @since 2.0.0 - * @category selection - */ - selectUtxos(strategy: CoinSelectionStrategyCIP2): Eff.Effect { - return Eff.gen( - function* (this: TransactionBuilder) { - const outputValue = this.calculateOutputValue() - const requiredValue = Value.add(outputValue, Value.onlyCoin(this.fee || Coin.make(BigInt(0)))) - - switch (strategy) { - case "LargestFirst": - yield* this.selectLargestFirst(requiredValue) - break - case "RandomImprove": - yield* this.selectRandomImprove(requiredValue) - break - case "RandomImproveMultiAsset": - yield* this.selectRandomImproveMultiAsset(requiredValue) - break - } - }.bind(this) - ) - } - - // ============================================================================ - // Building - // ============================================================================ - - /** - * Build the final signed transaction. - * - * @since 2.0.0 - * @category builders - */ - build( - changeAlgo: ChangeSelectionAlgo, - changeAddress: AddressEras.AddressEras - ): Eff.Effect { - return Eff.gen( - function* (this: TransactionBuilder) { - // Calculate and validate balance - yield* this.validateBalance() - - // Create change outputs if needed - const changeOutputs = yield* this.createChangeOutputs(changeAlgo, changeAddress) - - // Build transaction body - const body = yield* this.buildTransactionBody(changeOutputs) - - // Build witness set - const witnessSet = yield* this.buildWitnessSet(body) - - return new SignedTxBuilder({ - body, - witnessSet, - auxiliaryData: this.auxiliaryData - }) - }.bind(this) - ) - } - - /** - * Calculate minimum fee for the transaction. - * - * @since 2.0.0 - * @category fees - */ - minFee(): Eff.Effect { - return Eff.gen( - function* (this: TransactionBuilder) { - // Estimate transaction size with fake witnesses - const estimatedSize = yield* this.estimateTransactionSize() - - // Calculate linear fee - const baseFee = Coin.add(this.config.feeAlgo.constant, this.config.feeAlgo.coefficient * BigInt(estimatedSize)) - - // Add script execution fees if any - const scriptFee = yield* this.calculateScriptFees() - - return Coin.add(baseFee, scriptFee) - }.bind(this) - ) - } - - // ============================================================================ - // Private Implementation - // ============================================================================ - - private getOutputSize(result: SingleOutputBuilderResult): number { - // Calculate actual CBOR size of the output - try { - const cborBytes = TransactionOutput.toCBORBytes(result.output) - return cborBytes.length - } catch { - // Fall back to conservative estimate if encoding fails - return 200 - } - } - - private calculateOutputValue(): Value.Value { - return this.outputs.reduce( - (total: Value.Value, output) => Value.add(total, output.output.amount), - Value.onlyCoin(Coin.make(0n)) - ) - } - - private selectLargestFirst(requiredValue: Value.Value): Eff.Effect { - // Sort UTXOs by coin amount descending - const sortedUtxos = [...this.utxos].sort((a, b) => { - const coinA = Value.getAda(a.utxoInfo.amount) - const coinB = Value.getAda(b.utxoInfo.amount) - return Coin.compare(coinB, coinA) // Descending order - }) - - let selectedValue: Value.Value = Value.onlyCoin(Coin.make(0n)) - const selectedUtxos: Array = [] - - for (const utxo of sortedUtxos) { - selectedUtxos.push(utxo) - selectedValue = Value.add(selectedValue, utxo.utxoInfo.amount) - - if (Value.geq(selectedValue, requiredValue)) { - break - } - } - - if (!Value.geq(selectedValue, requiredValue)) { - return Eff.fail( - new TxBuilderError({ - message: "Insufficient funds for transaction" - }) - ) - } - - this.inputs.push(...selectedUtxos) - return Eff.succeed(undefined) - } - - private selectRandomImprove(requiredValue: Value.Value): Eff.Effect { - // Simplified random improve - select randomly first, then improve - return this.selectLargestFirst(requiredValue) - } - - private selectRandomImproveMultiAsset(requiredValue: Value.Value): Eff.Effect { - // Simplified multi-asset random improve - return this.selectLargestFirst(requiredValue) - } - - private validateBalance(): Eff.Effect { - const inputValue = this.inputs.reduce( - (total: Value.Value, input) => Value.add(total, input.utxoInfo.amount), - Value.onlyCoin(Coin.make(0n)) - ) - - const outputValue = this.calculateOutputValue() - const feeValue = Value.onlyCoin(this.fee || Coin.make(0n)) - const requiredValue = Value.add(outputValue, feeValue) - - if (!Value.geq(inputValue, requiredValue)) { - return Eff.fail( - new TxBuilderError({ - message: `Insufficient balance. Required: ${requiredValue}, Available: ${inputValue}` - }) - ) - } - - return Eff.succeed(undefined) - } - - private createChangeOutputs( - _algo: ChangeSelectionAlgo, - changeAddress: AddressEras.AddressEras - ): Eff.Effect, TxBuilderError> { - // Calculate change amount - const inputValue = this.inputs.reduce( - (total: Value.Value, input) => Value.add(total, input.utxoInfo.amount), - Value.onlyCoin(Coin.make(0n)) - ) - - const outputValue = this.calculateOutputValue() - const feeValue = Value.onlyCoin(this.fee || Coin.make(0n)) - const changeValue = Value.subtract(inputValue, Value.add(outputValue, feeValue)) - - // If no change needed, return empty array - const changeAmount = Value.getAda(changeValue) - if (Coin.equals(changeAmount, Coin.make(0n))) { - return Eff.succeed([]) - } - - // Create change output - const changeOutput = TransactionOutput.makeBabbage({ - address: changeAddress as any, // AddressEras includes reward addresses which aren't valid for outputs - amount: changeValue, - datumOption: undefined, - scriptRef: undefined - }) - - return Eff.succeed([ - { - output: changeOutput, - communicationDatum: undefined - } - ]) - } - - private buildTransactionBody( - changeOutputs: Array - ): Eff.Effect { - const allOutputs = [...this.outputs, ...changeOutputs] - - return Eff.succeed( - new TransactionBody.TransactionBody({ - inputs: this.inputs.map((r) => r.input), - outputs: allOutputs.map((r) => r.output), - fee: this.fee || Coin.make(0n), - ttl: this.ttl, - certificates: this.certificates.length > 0 ? (this.certificates.map((c) => c.cert) as any) : undefined, - withdrawals: this.withdrawals.length > 0 ? this.buildWithdrawals() : undefined, - auxiliaryDataHash: this.auxiliaryData ? Hash.hashAuxiliaryData(this.auxiliaryData) : undefined, - validityIntervalStart: this.validityStart, - mint: this.mints.length > 0 ? this.buildMint() : undefined, - scriptDataHash: undefined, // Will be calculated when script data is available - collateralInputs: - this.collateral.length > 0 - ? (this.collateral.map((c) => c.input) as NonEmptyArray) - : undefined, - requiredSigners: - this.requiredSigners.size > 0 - ? (Array.from(this.requiredSigners).map((hex) => KeyHash.fromHex(hex)) as NonEmptyArray) - : undefined, - networkId: this.networkId, - collateralReturn: undefined, // Would be set if using script collateral - totalCollateral: undefined, // Would be calculated based on script execution costs - referenceInputs: - this.referenceInputs.length > 0 - ? (this.referenceInputs.map((r) => r.input) as NonEmptyArray) - : undefined, - votingProcedures: undefined, // Will be implemented when VotingProcedures builder is ready - proposalProcedures: undefined, // Will be implemented when ProposalProcedures builder is ready - currentTreasuryValue: undefined, - donation: undefined - }) - ) - } - - private buildWithdrawals(): Withdrawals.Withdrawals | undefined { - if (this.withdrawals.length === 0) { - return undefined - } - - // Build withdrawals map from withdrawal builder results - const withdrawalMap = new Map() - for (const withdrawal of this.withdrawals) { - withdrawalMap.set(withdrawal.address, withdrawal.amount) - } - - return new Withdrawals.Withdrawals({ withdrawals: withdrawalMap }) - } - - private buildMint(): Mint.Mint | undefined { - if (this.mints.length === 0) { - return undefined - } - - // Combine all mint operations into a single Mint - const mintEntries: Array<[any, any]> = [] - - for (const mintResult of this.mints) { - // Convert assets map to NonZeroInt64 values - const assetEntries: Array<[any, any]> = [] - - for (const [assetName, amount] of mintResult.assets) { - // Only add non-zero amounts - if (amount !== 0n) { - try { - const nonZeroAmount = NonZeroInt64.make(amount.toString()) - assetEntries.push([assetName, nonZeroAmount]) - } catch { - // Skip if amount is zero or invalid - continue - } - } - } - - if (assetEntries.length > 0) { - mintEntries.push([mintResult.policyId, new Map(assetEntries)]) - } - } - - return mintEntries.length > 0 ? Mint.fromEntries(mintEntries) : undefined - } - - private buildWitnessSet( - _body: TransactionBody.TransactionBody - ): Eff.Effect { - // This would normally collect all witness data from inputs, mints, certificates, etc. - // For now, return an empty witness set - actual witnesses would be added during signing - return Eff.succeed( - new TransactionWitnessSet.TransactionWitnessSet({ - vkeyWitnesses: undefined, - nativeScripts: undefined, - bootstrapWitnesses: undefined, - plutusV1Scripts: undefined, - plutusData: undefined, - redeemers: undefined, - plutusV2Scripts: undefined, - plutusV3Scripts: undefined - }) - ) - } - - private estimateTransactionSize(): Eff.Effect { - // Conservative estimate based on typical transaction sizes - // Base size + input size + output size + witness size - const baseSize = 1500 - const inputSize = this.inputs.length * 150 - const outputSize = this.outputs.length * 200 - const witnessSize = this.requiredSigners.size * 100 - - return Eff.succeed(baseSize + inputSize + outputSize + witnessSize) - } - - private calculateScriptFees(): Eff.Effect { - // If no ExUnitPrices are configured, no script fees - if (!this.config.exUnitPrices) { - return Eff.succeed(Coin.make(0n)) - } - - // For now, return 0 fees - proper implementation would need to: - // 1. Collect ExUnits from all redeemers (inputs, mints, certificates, withdrawals) - // 2. Sum up the memory and steps - // 3. Calculate fee using the price model - // This requires the redeemer information to be properly tracked - // which would come from the script execution results - - return Eff.succeed(Coin.make(0n)) - } -} - -// ============================================================================ -// Effect Namespace - Effect-based Error Handling -// ============================================================================ - -/** - * Effect-based error handling variants for functions that can fail. - * Returns Effect for composable error handling. - * - * @since 2.0.0 - * @category effect - */ -export namespace Effect { - /** - * Create a new TransactionBuilderConfigBuilder using Effect error handling. - * - * @since 2.0.0 - * @category constructors - */ - export const newConfigBuilder = (): Eff.Effect => - Eff.succeed(TransactionBuilderConfigBuilder.new()) - - /** - * Create a new TransactionBuilder using Effect error handling. - * - * @since 2.0.0 - * @category constructors - */ - export const newBuilder = (config: TransactionBuilderConfig): Eff.Effect => - Eff.succeed(TransactionBuilder.new(config)) - - /** - * Create a new TransactionUnspentOutput using Effect error handling. - * - * @since 2.0.0 - * @category constructors - */ - export const newUtxo = ( - input: TransactionInput.TransactionInput, - output: TransactionOutput.TransactionOutput - ): Eff.Effect => Eff.succeed(TransactionUnspentOutput.new(input, output)) -} - -// ============================================================================ -// Root Namespace Functions (Sync API) -// ============================================================================ - -/** - * Create a new TransactionBuilderConfigBuilder. - * - * @since 2.0.0 - * @category constructors - */ -export const newConfigBuilder = (): TransactionBuilderConfigBuilder => TransactionBuilderConfigBuilder.new() - -/** - * Create a new TransactionBuilder. - * - * @since 2.0.0 - * @category constructors - */ -export const newBuilder = (config: TransactionBuilderConfig): TransactionBuilder => TransactionBuilder.new(config) - -/** - * Create a new TransactionUnspentOutput. - * - * @since 2.0.0 - * @category constructors - */ -export const newUtxo = ( - input: TransactionInput.TransactionInput, - output: TransactionOutput.TransactionOutput -): TransactionUnspentOutput => TransactionUnspentOutput.new(input, output) diff --git a/packages/evolution/src/builders/VoteBuilder.ts b/packages/evolution/src/builders/VoteBuilder.ts deleted file mode 100644 index e3b9fa3f..00000000 --- a/packages/evolution/src/builders/VoteBuilder.ts +++ /dev/null @@ -1,316 +0,0 @@ -import { Data, Effect as Eff } from "effect" - -import type * as PlutusData from "../core/Data.js" -import * as DatumOption from "../core/DatumOption.js" -import type * as KeyHash from "../core/KeyHash.js" -import type * as NativeScripts from "../core/NativeScripts.js" -import * as ScriptHash from "../core/ScriptHash.js" -import type * as VotingProcedures from "../core/VotingProcedures.js" -import { hashPlutusData } from "../utils/Hash.js" -import type { NativeScriptWitnessInfo, PartialPlutusWitness } from "./WitnessBuilder.js" -import { InputAggregateWitnessData, PlutusScriptWitness, RequiredWitnessSet } from "./WitnessBuilder.js" - -/** - * Error class for VoteBuilder related operations. - * - * @since 2.0.0 - * @category errors - */ -export class VoteBuilderError extends Data.TaggedError("VoteBuilderError")<{ - message?: string - cause?: unknown -}> {} - -// Define a simplified GovernanceActionId type for now -export interface GovActionId { - transactionId: string - govActionIndex: bigint -} - -// Define a simplified VotingProcedure type for now -export interface VotingProcedure { - vote: "No" | "Yes" | "Abstain" - anchor?: string -} - -/** - * Result of building votes - * - * @since 2.0.0 - * @category model - */ -export interface VoteBuilderResult { - votes: Map> - requiredWits: RequiredWitnessSet - aggregateWitnesses: Array -} - -/** - * Builder for governance votes - * - * @since 2.0.0 - * @category builders - */ -export class VoteBuilder { - private result: VoteBuilderResult - - constructor() { - this.result = { - votes: new Map(), - requiredWits: RequiredWitnessSet.default(), - aggregateWitnesses: [] - } - } - - static new(): VoteBuilder { - return new VoteBuilder() - } - - withVote( - voter: VotingProcedures.Voter, - govActionId: GovActionId, - procedure: VotingProcedure - ): Eff.Effect { - return Eff.gen( - function* (this: VoteBuilder) { - const keyHash = getVoterKeyHash(voter) - if (!keyHash) { - return yield* Eff.fail( - new VoteBuilderError({ - message: "Voter is script. Call withPlutusVote() instead." - }) - ) - } - - this.result.requiredWits.addVkeyKeyHash(keyHash) - - // Check for existing vote - const voterVotes = this.result.votes.get(voter) - if (voterVotes?.has(govActionId)) { - return yield* Eff.fail( - new VoteBuilderError({ - message: "Vote already exists" - }) - ) - } - - if (!voterVotes) { - this.result.votes.set(voter, new Map([[govActionId, procedure]])) - } else { - voterVotes.set(govActionId, procedure) - } - - return this - }.bind(this) - ) - } - - withNativeScriptVote( - voter: VotingProcedures.Voter, - govActionId: GovActionId, - procedure: VotingProcedure, - nativeScript: NativeScripts.NativeScript, - witnessInfo: NativeScriptWitnessInfo - ): Eff.Effect { - return Eff.gen( - function* (this: VoteBuilder) { - const voterScriptHash = getVoterScriptHash(voter) - const scriptHash = ScriptHash.fromScript(nativeScript) - - if (!voterScriptHash) { - return yield* Eff.fail( - new VoteBuilderError({ - message: "Voter is key hash. Call withVote() instead." - }) - ) - } - - if (!ScriptHash.equals(voterScriptHash, scriptHash)) { - const errRequiredWits = RequiredWitnessSet.default() - errRequiredWits.addScriptHash(voterScriptHash) - return yield* Eff.fail( - new VoteBuilderError({ - message: "Missing the following witnesses for the vote", - cause: errRequiredWits - }) - ) - } - - this.result.requiredWits.addScriptHash(voterScriptHash) - - // Check for existing vote - const voterVotes = this.result.votes.get(voter) - if (voterVotes?.has(govActionId)) { - return yield* Eff.fail( - new VoteBuilderError({ - message: "Vote already exists" - }) - ) - } - - if (!voterVotes) { - this.result.votes.set(voter, new Map([[govActionId, procedure]])) - } else { - voterVotes.set(govActionId, procedure) - } - - this.result.aggregateWitnesses.push(InputAggregateWitnessData.nativeScript(nativeScript, witnessInfo)) - - return this - }.bind(this) - ) - } - - withPlutusVote( - voter: VotingProcedures.Voter, - govActionId: GovActionId, - procedure: VotingProcedure, - partialWitness: PartialPlutusWitness, - requiredSigners: Array, - datum: PlutusData.Data - ): Eff.Effect { - return this.withPlutusVoteImpl(voter, govActionId, procedure, partialWitness, requiredSigners, datum) - } - - withPlutusVoteInlineDatum( - voter: VotingProcedures.Voter, - govActionId: GovActionId, - procedure: VotingProcedure, - partialWitness: PartialPlutusWitness, - requiredSigners: Array - ): Eff.Effect { - return this.withPlutusVoteImpl(voter, govActionId, procedure, partialWitness, requiredSigners, undefined) - } - - private withPlutusVoteImpl( - voter: VotingProcedures.Voter, - govActionId: GovActionId, - procedure: VotingProcedure, - partialWitness: PartialPlutusWitness, - requiredSigners: Array, - datum?: PlutusData.Data - ): Eff.Effect { - return Eff.gen( - function* (this: VoteBuilder) { - const requiredWits = RequiredWitnessSet.default() - requiredSigners.forEach((signer) => requiredWits.addVkeyKeyHash(signer)) - - const voterScriptHash = getVoterScriptHash(voter) - if (!voterScriptHash) { - return yield* Eff.fail( - new VoteBuilderError({ - message: "Voter is key hash. Call withVote() instead." - }) - ) - } - - requiredWits.addScriptHash(voterScriptHash) - const requiredWitsLeft = structuredClone(requiredWits) - - // Clear vkeys as we don't know which ones will be used - const clearedRequiredWitsLeft = new RequiredWitnessSet({ - vkeys: [], // Cleared - bootstraps: requiredWitsLeft.bootstraps, - scripts: requiredWitsLeft.scripts, - plutusData: requiredWitsLeft.plutusData, - redeemers: requiredWitsLeft.redeemers, - scriptRefs: requiredWitsLeft.scriptRefs - }) - - const scriptHash = PlutusScriptWitness.hash(partialWitness.scriptWitness) - - // Remove the script hash - const filteredScripts = clearedRequiredWitsLeft.scripts.filter((h) => !ScriptHash.equals(h, scriptHash)) - const updatedRequiredWitsLeft = new RequiredWitnessSet({ - vkeys: clearedRequiredWitsLeft.vkeys, - bootstraps: clearedRequiredWitsLeft.bootstraps, - scripts: filteredScripts, - plutusData: clearedRequiredWitsLeft.plutusData, - redeemers: clearedRequiredWitsLeft.redeemers, - scriptRefs: clearedRequiredWitsLeft.scriptRefs - }) - - // Remove datum hash if provided - let finalRequiredWitsLeft = updatedRequiredWitsLeft - if (datum) { - const datumHash = hashPlutusData(datum) - const filteredPlutusData = updatedRequiredWitsLeft.plutusData.filter((h) => !DatumOption.equals(h, datumHash)) - finalRequiredWitsLeft = new RequiredWitnessSet({ - vkeys: updatedRequiredWitsLeft.vkeys, - bootstraps: updatedRequiredWitsLeft.bootstraps, - scripts: updatedRequiredWitsLeft.scripts, - plutusData: filteredPlutusData, - redeemers: updatedRequiredWitsLeft.redeemers, - scriptRefs: updatedRequiredWitsLeft.scriptRefs - }) - } - - if (finalRequiredWitsLeft.len() > 0) { - return yield* Eff.fail( - new VoteBuilderError({ - message: "Missing the following witnesses for the vote", - cause: finalRequiredWitsLeft - }) - ) - } - - // Check for existing vote - const voterVotes = this.result.votes.get(voter) - if (voterVotes?.has(govActionId)) { - return yield* Eff.fail( - new VoteBuilderError({ - message: "Vote already exists" - }) - ) - } - - if (!voterVotes) { - this.result.votes.set(voter, new Map([[govActionId, procedure]])) - } else { - voterVotes.set(govActionId, procedure) - } - - this.result.requiredWits.addAll(requiredWits) - this.result.aggregateWitnesses.push( - InputAggregateWitnessData.plutusScript(partialWitness, requiredSigners, datum) - ) - - return this - }.bind(this) - ) - } - - build(): VoteBuilderResult { - return this.result - } -} - -/** - * Helper function to get key hash from a voter - * Returns undefined if voter uses script hash - */ -function getVoterKeyHash(voter: VotingProcedures.Voter): KeyHash.KeyHash | undefined { - // Extract KeyHash from voter credential - if (voter._tag === "ConstitutionalCommitteeVoter" && voter.credential._tag === "KeyHash") { - return voter.credential - } - if (voter._tag === "DRepVoter" && voter.drep._tag === "KeyHashDRep") { - return voter.drep.keyHash - } - return undefined -} - -/** - * Helper function to get script hash from a voter - * Returns undefined if voter uses key hash - */ -function getVoterScriptHash(voter: VotingProcedures.Voter): ScriptHash.ScriptHash | undefined { - // Extract ScriptHash from voter credential - if (voter._tag === "ConstitutionalCommitteeVoter" && voter.credential._tag === "ScriptHash") { - return voter.credential - } - if (voter._tag === "DRepVoter" && voter.drep._tag === "ScriptHashDRep") { - return voter.drep.scriptHash - } - return undefined -} diff --git a/packages/evolution/src/builders/WithdrawalBuilder.ts b/packages/evolution/src/builders/WithdrawalBuilder.ts deleted file mode 100644 index 8c680dc8..00000000 --- a/packages/evolution/src/builders/WithdrawalBuilder.ts +++ /dev/null @@ -1,195 +0,0 @@ -import { Data, Effect as Eff } from "effect" - -import type * as Coin from "../core/Coin.js" -import type * as KeyHash from "../core/KeyHash.js" -import type * as NativeScripts from "../core/NativeScripts.js" -import type * as RewardAccount from "../core/RewardAccount.js" -import * as ScriptHash from "../core/ScriptHash.js" -import type { NativeScriptWitnessInfo, PartialPlutusWitness } from "./WitnessBuilder.js" -import { InputAggregateWitnessData, PlutusScriptWitness, RequiredWitnessSet } from "./WitnessBuilder.js" - -/** - * Error class for WithdrawalBuilder related operations. - * - * @since 2.0.0 - * @category errors - */ -export class WithdrawalBuilderError extends Data.TaggedError("WithdrawalBuilderError")<{ - message?: string - cause?: unknown -}> {} - -/** - * Calculates required witnesses for a withdrawal - * - * @since 2.0.0 - * @category utils - */ -export function withdrawalRequiredWits( - address: RewardAccount.RewardAccount, - requiredWitnesses: RequiredWitnessSet -): void { - const credential = address.stakeCredential - - switch (credential._tag) { - case "KeyHash": - requiredWitnesses.addVkeyKeyHash(credential) - break - case "ScriptHash": - requiredWitnesses.addScriptHash(credential) - break - } -} - -/** - * Result of building a withdrawal - * - * @since 2.0.0 - * @category model - */ -export interface WithdrawalBuilderResult { - address: RewardAccount.RewardAccount - amount: Coin.Coin - aggregateWitness?: InputAggregateWitnessData - requiredWits: RequiredWitnessSet -} - -/** - * Builder for a single withdrawal - * - * @since 2.0.0 - * @category builders - */ -export class SingleWithdrawalBuilder { - constructor( - public readonly address: RewardAccount.RewardAccount, - public readonly amount: Coin.Coin - ) {} - - static new(address: RewardAccount.RewardAccount, amount: Coin.Coin): SingleWithdrawalBuilder { - return new SingleWithdrawalBuilder(address, amount) - } - - paymentKey(): Eff.Effect { - return Eff.gen( - function* (this: SingleWithdrawalBuilder) { - const requiredWits = RequiredWitnessSet.default() - withdrawalRequiredWits(this.address, requiredWits) - - if (requiredWits.scripts.length > 0) { - return yield* Eff.fail( - new WithdrawalBuilderError({ - message: "Withdrawal required a script, not a payment key" - }) - ) - } - - return { - address: this.address, - amount: this.amount, - aggregateWitness: undefined, - requiredWits - } - }.bind(this) - ) - } - - nativeScript( - nativeScript: NativeScripts.NativeScript, - witnessInfo: NativeScriptWitnessInfo - ): Eff.Effect { - return Eff.gen( - function* (this: SingleWithdrawalBuilder) { - const requiredWits = RequiredWitnessSet.default() - withdrawalRequiredWits(this.address, requiredWits) - const requiredWitsLeft = structuredClone(requiredWits) - - const scriptHash = ScriptHash.fromScript(nativeScript) - - // Remove the script hash from required witnesses - const filteredScripts = requiredWitsLeft.scripts.filter((h) => !ScriptHash.equals(h, scriptHash)) - const finalRequiredWitsLeft = new RequiredWitnessSet({ - vkeys: requiredWitsLeft.vkeys, - bootstraps: requiredWitsLeft.bootstraps, - scripts: filteredScripts, - plutusData: requiredWitsLeft.plutusData, - redeemers: requiredWitsLeft.redeemers, - scriptRefs: requiredWitsLeft.scriptRefs - }) - - if (finalRequiredWitsLeft.scripts.length > 0) { - return yield* Eff.fail( - new WithdrawalBuilderError({ - message: "Missing the following witnesses for the withdrawal", - cause: finalRequiredWitsLeft - }) - ) - } - - return { - address: this.address, - amount: this.amount, - aggregateWitness: InputAggregateWitnessData.nativeScript(nativeScript, witnessInfo), - requiredWits - } - }.bind(this) - ) - } - - plutusScript( - partialWitness: PartialPlutusWitness, - requiredSigners: Array - ): Eff.Effect { - return Eff.gen( - function* (this: SingleWithdrawalBuilder) { - const requiredWits = RequiredWitnessSet.default() - requiredSigners.forEach((signer) => requiredWits.addVkeyKeyHash(signer)) - withdrawalRequiredWits(this.address, requiredWits) - const requiredWitsLeft = structuredClone(requiredWits) - - // Clear vkeys as we don't know which ones will be used - const clearedRequiredWitsLeft = new RequiredWitnessSet({ - vkeys: [], // Cleared - bootstraps: requiredWitsLeft.bootstraps, - scripts: requiredWitsLeft.scripts, - plutusData: requiredWitsLeft.plutusData, - redeemers: requiredWitsLeft.redeemers, - scriptRefs: requiredWitsLeft.scriptRefs - }) - - const scriptHash = PlutusScriptWitness.hash(partialWitness.scriptWitness) - - // Remove the script hash - const filteredScripts = clearedRequiredWitsLeft.scripts.filter((h) => !ScriptHash.equals(h, scriptHash)) - const finalRequiredWitsLeft = new RequiredWitnessSet({ - vkeys: clearedRequiredWitsLeft.vkeys, - bootstraps: clearedRequiredWitsLeft.bootstraps, - scripts: filteredScripts, - plutusData: clearedRequiredWitsLeft.plutusData, - redeemers: clearedRequiredWitsLeft.redeemers, - scriptRefs: clearedRequiredWitsLeft.scriptRefs - }) - - if (finalRequiredWitsLeft.len() > 0) { - return yield* Eff.fail( - new WithdrawalBuilderError({ - message: "Missing the following witnesses for the withdrawal", - cause: finalRequiredWitsLeft - }) - ) - } - - return { - address: this.address, - amount: this.amount, - aggregateWitness: InputAggregateWitnessData.plutusScript( - partialWitness, - requiredSigners, - undefined // No datum for withdrawals - ), - requiredWits - } - }.bind(this) - ) - } -} diff --git a/packages/evolution/src/builders/WitnessBuilder.ts b/packages/evolution/src/builders/WitnessBuilder.ts deleted file mode 100644 index efe344c8..00000000 --- a/packages/evolution/src/builders/WitnessBuilder.ts +++ /dev/null @@ -1,242 +0,0 @@ -import { Data, Schema } from "effect" - -import * as ByronAddress from "../core/ByronAddress.js" -import type * as PlutusData from "../core/Data.js" -import * as DatumOption from "../core/DatumOption.js" -import * as KeyHash from "../core/KeyHash.js" -import type * as NativeScripts from "../core/NativeScripts.js" -import type * as PlutusV1 from "../core/PlutusV1.js" -import type * as PlutusV2 from "../core/PlutusV2.js" -import type * as PlutusV3 from "../core/PlutusV3.js" -import * as Redeemer from "../core/Redeemer.js" -import * as ScriptHash from "../core/ScriptHash.js" - -/** - * Error class for WitnessBuilder related operations. - * - * @since 2.0.0 - * @category errors - */ -export class WitnessBuilderError extends Data.TaggedError("WitnessBuilderError")<{ - message?: string - cause?: unknown -}> {} - -/** - * Redeemer witness key for identifying redeemers by tag and index. - * - * @since 2.0.0 - * @category model - */ -export class RedeemerWitnessKey extends Schema.Class("RedeemerWitnessKey")({ - tag: Redeemer.RedeemerTag, - index: Schema.BigInt.annotations({ - identifier: "RedeemerWitnessKey.Index", - description: "Index into the respective transaction array" - }) -}) { - static new(tag: Redeemer.RedeemerTag, index: bigint): RedeemerWitnessKey { - return new RedeemerWitnessKey({ tag, index }) - } -} - -/** - * Required witness set tracking what witnesses are needed - * - * @since 2.0.0 - * @category model - */ -export class RequiredWitnessSet extends Schema.Class("RequiredWitnessSet")({ - vkeys: Schema.Array(KeyHash.KeyHash), - bootstraps: Schema.Array(ByronAddress.ByronAddress), - scripts: Schema.Array(ScriptHash.ScriptHash), - plutusData: Schema.Array(DatumOption.DatumHash), - redeemers: Schema.Array(RedeemerWitnessKey), - scriptRefs: Schema.Array(ScriptHash.ScriptHash) -}) { - static default(): RequiredWitnessSet { - return new RequiredWitnessSet({ - vkeys: [], - bootstraps: [], - scripts: [], - plutusData: [], - redeemers: [], - scriptRefs: [] - }) - } - - addVkeyKeyHash(hash: KeyHash.KeyHash): void { - if (!this.vkeys.find((h) => KeyHash.equals(h, hash))) { - ;(this.vkeys as Array).push(hash) - } - } - - addBootstrap(address: ByronAddress.ByronAddress): void { - if (!this.bootstraps.find((a) => ByronAddress.equals(a, address))) { - ;(this.bootstraps as Array).push(address) - } - } - - addScriptHash(hash: ScriptHash.ScriptHash): void { - // Check if it's already in script refs - if (!this.scriptRefs.find((h) => ScriptHash.equals(h, hash))) { - if (!this.scripts.find((h) => ScriptHash.equals(h, hash))) { - ;(this.scripts as Array).push(hash) - } - } - } - - addScriptRef(hash: ScriptHash.ScriptHash): void { - // Remove from scripts if present - ;(this as any).scripts = this.scripts.filter((h) => !ScriptHash.equals(h, hash)) - if (!this.scriptRefs.find((h) => ScriptHash.equals(h, hash))) { - ;(this.scriptRefs as Array).push(hash) - } - } - - addPlutusDataHash(hash: DatumOption.DatumHash): void { - if (!this.plutusData.find((h) => DatumOption.equals(h, hash))) { - ;(this.plutusData as Array).push(hash) - } - } - - addRedeemerTag(redeemer: RedeemerWitnessKey): void { - if (!this.redeemers.find((r) => r.tag === redeemer.tag && r.index === redeemer.index)) { - ;(this.redeemers as Array).push(redeemer) - } - } - - addAll(requirements: RequiredWitnessSet): void { - requirements.vkeys.forEach((vkey) => this.addVkeyKeyHash(vkey)) - requirements.bootstraps.forEach((bootstrap) => this.addBootstrap(bootstrap)) - requirements.scripts.forEach((script) => this.addScriptHash(script)) - requirements.plutusData.forEach((data) => this.addPlutusDataHash(data)) - requirements.redeemers.forEach((redeemer) => this.addRedeemerTag(redeemer)) - requirements.scriptRefs.forEach((ref) => this.addScriptRef(ref)) - } - - len(): number { - return ( - this.vkeys.length + - this.bootstraps.length + - this.scripts.length + - this.plutusData.length + - this.redeemers.length + - this.scriptRefs.length - ) - } -} - -/** - * Native script witness info - * - * @since 2.0.0 - * @category model - */ -export type NativeScriptWitnessInfo = - | { type: "Count"; num: number } - | { type: "Vkeys"; vkeys: Array } - | { type: "AssumeWorst" } - -export const NativeScriptWitnessInfo = { - numSignatures(num: number): NativeScriptWitnessInfo { - return { type: "Count", num } - }, - - vkeys(vkeys: Array): NativeScriptWitnessInfo { - return { type: "Vkeys", vkeys } - }, - - assumeSignatureCount(): NativeScriptWitnessInfo { - return { type: "AssumeWorst" } - } -} - -/** - * Plutus script witness - * - * @since 2.0.0 - * @category model - */ -export type PlutusScriptWitness = - | { type: "Ref"; hash: ScriptHash.ScriptHash } - | { type: "Script"; script: PlutusV1.PlutusV1 | PlutusV2.PlutusV2 | PlutusV3.PlutusV3 } - -export const PlutusScriptWitness = { - ref(hash: ScriptHash.ScriptHash): PlutusScriptWitness { - return { type: "Ref", hash } - }, - - script(script: PlutusV1.PlutusV1 | PlutusV2.PlutusV2 | PlutusV3.PlutusV3): PlutusScriptWitness { - return { type: "Script", script } - }, - - hash(witness: PlutusScriptWitness): ScriptHash.ScriptHash { - switch (witness.type) { - case "Ref": - return witness.hash - case "Script": - // Use ScriptHash.fromScript to compute the hash - return ScriptHash.fromScript(witness.script) - } - } -} - -/** - * Partial plutus witness - * - * @since 2.0.0 - * @category model - */ -export class PartialPlutusWitness extends Schema.Class("PartialPlutusWitness")({ - script: Schema.Any, // PlutusScriptWitness - redeemer: Schema.Any // PlutusData.Data -}) { - static new(script: PlutusScriptWitness, redeemer: PlutusData.Data): PartialPlutusWitness { - return new PartialPlutusWitness({ script: script as any, redeemer }) - } - - get scriptWitness(): PlutusScriptWitness { - return this.script as PlutusScriptWitness - } - - get redeemerData(): PlutusData.Data { - return this.redeemer as PlutusData.Data - } -} - -/** - * Aggregate witness data for inputs - * - * @since 2.0.0 - * @category model - */ -export type InputAggregateWitnessData = - | { type: "NativeScript"; script: NativeScripts.NativeScript; info: NativeScriptWitnessInfo } - | { - type: "PlutusScript" - witness: PartialPlutusWitness - requiredSigners: Array - datum?: PlutusData.Data - } - -export const InputAggregateWitnessData = { - nativeScript(script: NativeScripts.NativeScript, info: NativeScriptWitnessInfo): InputAggregateWitnessData { - return { type: "NativeScript", script, info } - }, - - plutusScript( - witness: PartialPlutusWitness, - requiredSigners: Array, - datum?: PlutusData.Data - ): InputAggregateWitnessData { - return { type: "PlutusScript", witness, requiredSigners, datum } - }, - - redeemerPlutusData(data: InputAggregateWitnessData): PlutusData.Data | undefined { - if (data.type === "PlutusScript") { - return data.witness.redeemer - } - return undefined - } -} diff --git a/packages/evolution/src/builders/index.ts b/packages/evolution/src/builders/index.ts deleted file mode 100644 index 8aef0aca..00000000 --- a/packages/evolution/src/builders/index.ts +++ /dev/null @@ -1,38 +0,0 @@ -/** - * Transaction builder modules for creating transaction components with witness information - * - * @since 2.0.0 - */ - -// Core witness building utilities -export * from "./WitnessBuilder.js" - -// Input builder for transaction inputs -export * from "./InputBuilder.js" - -// Mint builder for minting operations -export * from "./MintBuilder.js" - -// Withdrawal builder for stake reward withdrawals -export * from "./WithdrawalBuilder.js" - -// Certificate builder for stake pool and delegation certificates -export * from "./CertificateBuilder.js" - -// Proposal builder for governance proposals -export * from "./ProposalBuilder.js" - -// Vote builder for governance votes -export * from "./VoteBuilder.js" - -// Redeemer builder for Plutus script redeemers -export * from "./RedeemerBuilder.js" - -// Output builder for transaction outputs -export * from "./OutputBuilder.js" - -// Transaction builder for complete transactions -export * from "./TxBuilder.js" - -// Builder utilities -export * from "./utils/index.js" diff --git a/packages/evolution/src/builders/utils/MinAda.ts b/packages/evolution/src/builders/utils/MinAda.ts deleted file mode 100644 index b11ba32d..00000000 --- a/packages/evolution/src/builders/utils/MinAda.ts +++ /dev/null @@ -1,178 +0,0 @@ -import { Data, Effect as Eff } from "effect" - -import type * as Coin from "../../core/Coin.js" -import * as TransactionOutput from "../../core/TransactionOutput.js" -import * as Value from "../../core/Value.js" - -/** - * Error class for MinAda calculation related operations. - * - * @since 2.0.0 - * @category errors - */ -export class MinAdaError extends Data.TaggedError("MinAdaError")<{ - message?: string - cause?: unknown -}> {} - -/** - * Calculate the CBOR encoding size for a coin value. - * Based on CBOR specification for unsigned integers. - * This matches the `fit_sz` function in CML Rust implementation. - * - * @since 2.0.0 - * @category utils - */ -const getCoinCborSize = (coin: Coin.Coin): number => { - const value = coin - - // CBOR unsigned integer encoding: - // - 0-23: direct encoding (1 byte total including type) - // - 24-255: 1 byte + 1 byte value = 2 bytes - // - 256-65535: 1 byte + 2 byte value = 3 bytes - // - 65536-4294967295: 1 byte + 4 byte value = 5 bytes - // - Above: 1 byte + 8 byte value = 9 bytes - if (value <= 23n) return 1 - if (value <= 255n) return 2 - if (value <= 65535n) return 3 - if (value <= 4294967295n) return 5 - return 9 -} - -/** - * Calculate minimum ADA required for a transaction output. - * Direct port of the Rust implementation from cardano-multiplatform-lib. - * - * Algorithm matches CML's min_ada.rs: - * 1. Calculate CBOR size of the output - * 2. Add 160-byte constant overhead (from Babbage spec figure 5) - * 3. Use iterative approach to handle coin size changes affecting CBOR encoding - * 4. Multiply total size by coins_per_utxo_byte protocol parameter - * - * @since 2.0.0 - * @category calculations - */ -export const minAdaRequired = ( - output: TransactionOutput.TransactionOutput, - coinsPerUtxoByte: Coin.Coin -): Eff.Effect => - Eff.gen(function* () { - try { - // Get CBOR size of the output (matches output.to_cbor_bytes().len()) - const outputCborBytes = yield* Eff.try({ - try: () => TransactionOutput.toCBORBytes(output), - catch: (cause) => - new MinAdaError({ - message: "Failed to serialize output to CBOR", - cause - }) - }) - - const outputSize = outputCborBytes.length - - // Constant from figure 5 in Babbage spec meant to represent the size the input in a UTXO - const constantOverhead = 160 - - // Extract current coin amount from the output - const currentCoin = Value.getAda(output.amount) - - // How many bytes the Coin part of the Value will take (matches old_coin_size calculation) - const oldCoinSize = getCoinCborSize(currentCoin) - - // Most recent estimate of the size in bytes to include the minimum ADA value - let latestSize = oldCoinSize - - // We calculate min ada in a loop because every time we increase the min ADA, - // it may increase the CBOR size in bytes - let tentativeMinAda: Coin.Coin - - while (true) { - const sizeDiff = latestSize - oldCoinSize - - // Calculate tentative minimum ADA - const totalSizeForCalc = outputSize + constantOverhead + sizeDiff - - // Check for overflow (matches the Rust checked_mul logic) - if (totalSizeForCalc < 0 || totalSizeForCalc > Number.MAX_SAFE_INTEGER) { - return yield* Eff.fail( - new MinAdaError({ - message: "Integer overflow in minimum ADA calculation" - }) - ) - } - - tentativeMinAda = BigInt(totalSizeForCalc) * coinsPerUtxoByte - - // Calculate new coin CBOR size (matches new_coin_size calculation) - const newCoinSize = getCoinCborSize(tentativeMinAda) - - // Check if we've converged - const isDone = latestSize === newCoinSize - latestSize = newCoinSize - - if (isDone) { - break - } - } - - // How many bytes the size changed from including the minimum ADA value - const sizeChange = latestSize - oldCoinSize - - // Final calculation with converged size - const finalTotalSize = outputSize + constantOverhead + sizeChange - - // Check for overflow again - if (finalTotalSize < 0 || finalTotalSize > Number.MAX_SAFE_INTEGER) { - return yield* Eff.fail( - new MinAdaError({ - message: "Integer overflow in final minimum ADA calculation" - }) - ) - } - - const adjustedMinAda = BigInt(finalTotalSize) * coinsPerUtxoByte - - return adjustedMinAda - } catch (error) { - return yield* Eff.fail( - new MinAdaError({ - message: "Unexpected error in minimum ADA calculation", - cause: error - }) - ) - } - }) - -/** - * Calculate minimum ADA required for a transaction output (sync version). - * - * @since 2.0.0 - * @category calculations - */ -export const minAdaRequiredSync = ( - output: TransactionOutput.TransactionOutput, - coinsPerUtxoByte: Coin.Coin -): Coin.Coin => Eff.runSync(minAdaRequired(output, coinsPerUtxoByte)) - -// ============================================================================ -// Effect Namespace -// ============================================================================ - -/** - * Effect-based error handling variants for functions that can fail. - * - * @since 2.0.0 - * @category effect - */ -export namespace MinAdaEffect { - /** - * Calculate minimum ADA required for a transaction output using Effect error handling. - * - * @since 2.0.0 - * @category calculations - */ - export const minAdaRequired = ( - output: TransactionOutput.TransactionOutput, - coinsPerUtxoByte: Coin.Coin - ): Eff.Effect => minAdaRequired(output, coinsPerUtxoByte) -} diff --git a/packages/evolution/src/builders/utils/index.ts b/packages/evolution/src/builders/utils/index.ts deleted file mode 100644 index b2f0a2e6..00000000 --- a/packages/evolution/src/builders/utils/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./MinAda.js"