diff --git a/.changeset/cruel-wasps-worry.md b/.changeset/cruel-wasps-worry.md new file mode 100644 index 00000000..e597d94d --- /dev/null +++ b/.changeset/cruel-wasps-worry.md @@ -0,0 +1,5 @@ +--- +"@evolution-sdk/aiken-uplc": patch +--- + +Initial release of Aiken UPLC evaluator - a WASM-based plugin for local script evaluation in the Evolution SDK diff --git a/.changeset/odd-cooks-jump.md b/.changeset/odd-cooks-jump.md new file mode 100644 index 00000000..5eb3ed27 --- /dev/null +++ b/.changeset/odd-cooks-jump.md @@ -0,0 +1,52 @@ +--- +"@evolution-sdk/devnet": patch +"@evolution-sdk/evolution": patch +--- + +### TxBuilder Composition API + +Add `compose()` and `getPrograms()` methods for modular transaction building: + +```ts +// Create reusable builder fragments +const mintBuilder = client.newTx() + .mintAssets({ policyId, assets: { tokenName: 1n }, redeemer }) + .attachScript({ script: mintingPolicy }) + +const metadataBuilder = client.newTx() + .attachMetadata({ label: 674n, metadata: "Cross-chain tx" }) + +// Compose multiple builders into one transaction +const tx = await client.newTx() + .payToAddress({ address, assets: { lovelace: 5_000_000n } }) + .compose(mintBuilder) + .compose(metadataBuilder) + .build() +``` + +**Features:** +- Merge operations from multiple builders into a single transaction +- Snapshot accumulated operations with `getPrograms()` for inspection +- Compose builders from different client instances +- Works with all builder methods (payments, validity, metadata, minting, staking, etc.) + +### Fixed Validity Interval Fee Calculation Bug + +Fixed bug where validity interval fields (`ttl` and `validityIntervalStart`) were not included during fee calculation, causing "insufficient fee" errors when using `setValidity()`. + +**Root Cause**: Validity fields were being added during transaction assembly AFTER fee calculation completed, causing the actual transaction to be 3-8 bytes larger than estimated. + +**Fix**: Convert validity Unix times to slots BEFORE the fee calculation loop and include them in the TransactionBody during size estimation. + +### Error Type Corrections + +Corrected error types for pure constructor functions to use `never` instead of `TransactionBuilderError`: +- `makeTxOutput` - creates TransactionOutput +- `txOutputToTransactionOutput` - creates TransactionOutput +- `mergeAssetsIntoUTxO` - creates UTxO +- `mergeAssetsIntoOutput` - creates TransactionOutput +- `buildTransactionInputs` - creates and sorts TransactionInputs + +### Error Message Improvements + +Enhanced error messages throughout the builder to include underlying error details for better debugging. diff --git a/docs/content/docs/modules/core/Metadata.mdx b/docs/content/docs/modules/core/Metadata.mdx index 667d6815..98b13e59 100644 --- a/docs/content/docs/modules/core/Metadata.mdx +++ b/docs/content/docs/modules/core/Metadata.mdx @@ -497,8 +497,8 @@ Added in v2.0.0 ## MetadataLabel -Schema for transaction metadatum label (uint - unbounded positive integer). -Uses Numeric.NonNegativeInteger for consistency with other numeric types. +Schema for transaction metadatum label (uint64 per Cardano CDDL spec). +Labels must be in range 0 to 2^64-1. **Signature** diff --git a/docs/content/docs/modules/sdk/builders/TransactionBuilder.mdx b/docs/content/docs/modules/sdk/builders/TransactionBuilder.mdx index 461418bb..d81bbe20 100644 --- a/docs/content/docs/modules/sdk/builders/TransactionBuilder.mdx +++ b/docs/content/docs/modules/sdk/builders/TransactionBuilder.mdx @@ -55,8 +55,6 @@ double-spending. UTxOs can come from any source (wallet, DeFi protocols, other p - [formatFailures (method)](#formatfailures-method) - [ScriptFailure (interface)](#scriptfailure-interface) - [TransactionBuilderError (class)](#transactionbuildererror-class) -- [evaluators](#evaluators) - - [createUPLCEvaluator](#createuplcevaluator) - [model](#model) - [ChainResult (interface)](#chainresult-interface) - [EvaluationContext (interface)](#evaluationcontext-interface) @@ -67,7 +65,6 @@ double-spending. UTxOs can come from any source (wallet, DeFi protocols, other p - [TxBuilderState (interface)](#txbuilderstate-interface) - [types](#types) - [ProgramStep (type alias)](#programstep-type-alias) - - [UPLCEvalFunction (type alias)](#uplcevalfunction-type-alias) - [utils](#utils) - [BuildOptions (interface)](#buildoptions-interface) - [PhaseContextTag (class)](#phasecontexttag-class) @@ -633,7 +630,7 @@ export interface TransactionBuilderBase { /** * Attach metadata to the transaction. * - * Metadata is stored in the auxiliary data section and identified by labels (0-255) + * Metadata is stored in the auxiliary data section and identified by numeric labels * following the CIP-10 standard. Common use cases include: * - Transaction messages/comments (label 674, CIP-20) * - NFT metadata (label 721, CIP-25) @@ -648,28 +645,19 @@ export interface TransactionBuilderBase { * * @example * ```typescript - * import * as TransactionMetadatum from "@evolution-sdk/core/TransactionMetadatum" + * import { fromEntries } from "@evolution-sdk/evolution/core/TransactionMetadatum" * * // Attach a simple message (CIP-20) * const tx = await builder * .payToAddress({ address, assets: { lovelace: 2_000_000n } }) - * .attachMetadata({ - * label: 674n, - * metadata: TransactionMetadatum.makeTransactionMetadatumMap( - * new Map([[0n, TransactionMetadatum.makeTransactionMetadatumText("Hello, Cardano!")]]) - * ) - * }) + * .attachMetadata({ label: 674n, metadata: "Hello, Cardano!" }) * .build() * * // Attach NFT metadata (CIP-25) - * const nftMetadata = TransactionMetadatum.makeTransactionMetadatumMap( - * new Map([ - * [TransactionMetadatum.makeTransactionMetadatumText("name"), - * TransactionMetadatum.makeTransactionMetadatumText("My NFT #42")], - * [TransactionMetadatum.makeTransactionMetadatumText("image"), - * TransactionMetadatum.makeTransactionMetadatumText("ipfs://Qm...")], - * ]) - * ) + * const nftMetadata = fromEntries([ + * ["name", "My NFT #42"], + * ["image", "ipfs://Qm..."] + * ]) * const tx = await builder * .mintAssets({ assets: { [policyId + assetName]: 1n } }) * .attachMetadata({ label: 721n, metadata: nftMetadata }) @@ -681,6 +669,65 @@ export interface TransactionBuilderBase { */ readonly attachMetadata: (params: AttachMetadataParams) => this + // ============================================================================ + // Composition Methods + // ============================================================================ + + /** + * Compose this builder with another builder's accumulated operations. + * + * Merges all queued operations from another transaction builder into this one. + * The other builder's programs are captured at compose time and will be executed + * when build() is called on this builder. + * + * This enables modular transaction building where common patterns can be + * encapsulated in reusable builder fragments. + * + * **Important**: Composition is one-way - changes to the other builder after + * compose() is called will not affect this builder. + * + * @example + * ```typescript + * // Create reusable builder for common operations + * const mintBuilder = builder + * .mintAssets({ policyId, assets: { tokenName: 1n }, redeemer }) + * .attachScript({ script: mintingPolicy }) + * + * // Compose into a transaction that also pays to an address + * const tx = await builder + * .payToAddress({ address, assets: { lovelace: 5_000_000n } }) + * .compose(mintBuilder) + * .build() + * + * // Compose multiple builders + * const fullTx = await builder + * .compose(mintBuilder) + * .compose(metadataBuilder) + * .compose(certBuilder) + * .build() + * ``` + * + * @param other - Another transaction builder whose operations will be merged + * @returns The same builder for method chaining + * + * @since 2.0.0 + * @category composition-methods + */ + readonly compose: (other: TransactionBuilder) => this + + /** + * Get a snapshot of the accumulated programs. + * + * Returns a read-only copy of all queued operations that have been added + * to this builder. Useful for inspection, debugging, or advanced composition patterns. + * + * @returns Read-only array of accumulated program steps + * + * @since 2.0.0 + * @category composition-methods + */ + readonly getPrograms: () => ReadonlyArray + // ============================================================================ // Transaction Chaining Methods // ============================================================================ @@ -1079,23 +1126,6 @@ export declare class TransactionBuilderError Added in v2.0.0 -# evaluators - -## createUPLCEvaluator - -Creates an evaluator from a standard UPLC evaluation function. - -**NOTE: NOT YET IMPLEMENTED** - This function currently returns an evaluator -that produces dummy data. Reserved for future UPLC script evaluation support. - -**Signature** - -```ts -export declare const createUPLCEvaluator: (_evalFunction: UPLCEvalFunction) => Evaluator -``` - -Added in v2.0.0 - # model ## ChainResult (interface) @@ -1168,7 +1198,7 @@ export interface Evaluator { * @category methods */ evaluate: ( - tx: string, + tx: Transaction.Transaction, additionalUtxos: ReadonlyArray | undefined, context: EvaluationContext ) => Effect.Effect, EvaluationError> @@ -1301,30 +1331,6 @@ export type ProgramStep = Effect.Effect, - utxos_bytes_y: Array, - cost_mdls_bytes: Uint8Array, - initial_budget_n: bigint, - initial_budget_d: bigint, - slot_config_x: bigint, - slot_config_y: bigint, - slot_config_z: number -) => Array -``` - -Added in v2.0.0 - # utils ## BuildOptions (interface) diff --git a/docs/content/docs/modules/sdk/builders/TxBuilderImpl.mdx b/docs/content/docs/modules/sdk/builders/TxBuilderImpl.mdx index 5de6e022..b66b43db 100644 --- a/docs/content/docs/modules/sdk/builders/TxBuilderImpl.mdx +++ b/docs/content/docs/modules/sdk/builders/TxBuilderImpl.mdx @@ -78,7 +78,7 @@ Uses Core UTxO types directly. ```ts export declare const buildTransactionInputs: ( utxos: ReadonlyArray -) => Effect.Effect, TransactionBuilderError> +) => Effect.Effect, never> ``` Added in v2.0.0 @@ -187,7 +187,7 @@ export declare const calculateFeeIteratively: ( } >, protocolParams: { minFeeCoefficient: bigint; minFeeConstant: bigint; priceMem?: number; priceStep?: number } -) => Effect.Effect +) => Effect.Effect ``` Added in v2.0.0 @@ -350,7 +350,7 @@ export declare const makeTxOutput: (params: { assets: CoreAssets.Assets datum?: DatumOption.DatumOption scriptRef?: CoreScript.Script -}) => Effect.Effect +}) => Effect.Effect ``` Added in v2.0.0 @@ -368,7 +368,7 @@ Use case: Draining wallet by merging leftover into an existing payment output. export declare const mergeAssetsIntoOutput: ( output: TxOut.TransactionOutput, additionalAssets: CoreAssets.Assets -) => Effect.Effect +) => Effect.Effect ``` Added in v2.0.0 @@ -386,7 +386,7 @@ Use case: Draining wallet by merging leftover into an existing payment output. export declare const mergeAssetsIntoUTxO: ( utxo: CoreUTxO.UTxO, additionalAssets: CoreAssets.Assets -) => Effect.Effect +) => Effect.Effect ``` Added in v2.0.0 diff --git a/docs/content/docs/modules/sdk/builders/operations/Attach.mdx b/docs/content/docs/modules/sdk/builders/operations/Attach.mdx index 5888e7f0..05d15af5 100644 --- a/docs/content/docs/modules/sdk/builders/operations/Attach.mdx +++ b/docs/content/docs/modules/sdk/builders/operations/Attach.mdx @@ -28,9 +28,7 @@ Scripts must be attached before being referenced by transaction inputs or mintin **Signature** ```ts -export declare const attachScriptToState: ( - script: ScriptCore.Script -) => Effect.Effect +export declare const attachScriptToState: (script: ScriptCore.Script) => Effect.Effect ``` Added in v2.0.0 diff --git a/docs/content/docs/modules/sdk/builders/operations/AttachMetadata.mdx b/docs/content/docs/modules/sdk/builders/operations/AttachMetadata.mdx index 705927a5..cfb2f06a 100644 --- a/docs/content/docs/modules/sdk/builders/operations/AttachMetadata.mdx +++ b/docs/content/docs/modules/sdk/builders/operations/AttachMetadata.mdx @@ -42,7 +42,7 @@ Implementation: ```ts export declare const createAttachMetadataProgram: ( params: AttachMetadataParams -) => Effect.Effect +) => Effect.Effect ``` Added in v2.0.0 diff --git a/docs/content/docs/modules/sdk/builders/operations/Pay.mdx b/docs/content/docs/modules/sdk/builders/operations/Pay.mdx index 7cae2885..e7f9857c 100644 --- a/docs/content/docs/modules/sdk/builders/operations/Pay.mdx +++ b/docs/content/docs/modules/sdk/builders/operations/Pay.mdx @@ -35,9 +35,7 @@ Implementation: **Signature** ```ts -export declare const createPayToAddressProgram: ( - params: PayToAddressParams -) => Effect.Effect +export declare const createPayToAddressProgram: (params: PayToAddressParams) => Effect.Effect ``` Added in v2.0.0 diff --git a/docs/content/docs/modules/sdk/builders/phases/FeeCalculation.mdx b/docs/content/docs/modules/sdk/builders/phases/FeeCalculation.mdx index 3cf41edc..21f2b5a7 100644 --- a/docs/content/docs/modules/sdk/builders/phases/FeeCalculation.mdx +++ b/docs/content/docs/modules/sdk/builders/phases/FeeCalculation.mdx @@ -68,6 +68,6 @@ goto balance export declare const executeFeeCalculation: () => Effect.Effect< PhaseResult, TransactionBuilderError, - PhaseContextTag | TxContext | ProtocolParametersTag + PhaseContextTag | TxContext | ProtocolParametersTag | BuildOptionsTag > ``` diff --git a/packages/aiken-uplc/.gitignore b/packages/aiken-uplc/.gitignore new file mode 100644 index 00000000..7a8cf801 --- /dev/null +++ b/packages/aiken-uplc/.gitignore @@ -0,0 +1 @@ +rust/target \ No newline at end of file diff --git a/packages/aiken-uplc/flake.lock b/packages/aiken-uplc/flake.lock new file mode 100644 index 00000000..734bc47c --- /dev/null +++ b/packages/aiken-uplc/flake.lock @@ -0,0 +1,60 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1766651565, + "narHash": "sha256-QEhk0eXgyIqTpJ/ehZKg9IKS7EtlWxF3N7DXy42zPfU=", + "rev": "3e2499d5539c16d0d173ba53552a4ff8547f4539", + "revCount": 916364, + "type": "tarball", + "url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.1.916364%2Brev-3e2499d5539c16d0d173ba53552a4ff8547f4539/019b55f3-ffa1-7567-85ec-0561fc22d452/source.tar.gz" + }, + "original": { + "type": "tarball", + "url": "https://flakehub.com/f/NixOS/nixpkgs/0.1.%2A.tar.gz" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1744536153, + "narHash": "sha256-awS2zRgF4uTwrOKwwiJcByDzDOdo3Q1rPZbiHQg/N38=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "18dd725c29603f582cf1900e0d25f9f1063dbf11", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs", + "rust-overlay": "rust-overlay" + } + }, + "rust-overlay": { + "inputs": { + "nixpkgs": "nixpkgs_2" + }, + "locked": { + "lastModified": 1766717007, + "narHash": "sha256-ZjLiHCHgoH2maP5ZAKn0anrHymbjGOS5/PZqfJUK8Ik=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "a18efe8a9112175e43397cf870fb6bc1ca480548", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/packages/aiken-uplc/flake.nix b/packages/aiken-uplc/flake.nix new file mode 100644 index 00000000..6ed43eb5 --- /dev/null +++ b/packages/aiken-uplc/flake.nix @@ -0,0 +1,81 @@ +{ + description = "A Nix-flake-based Rust development environment"; + + nixConfig = { + bash-prompt = "\\[\\e[0;92m\\][\\[\\e[0;92m\\]nix develop:\\[\\e[0;92m\\]\\w\\[\\e[0;92m\\]]\\[\\e[0;92m\\]$ \\[\\e[0m\\]"; + }; + + inputs = { + nixpkgs.url = "https://flakehub.com/f/NixOS/nixpkgs/0.1.*.tar.gz"; + + + rust-overlay.url = "github:oxalica/rust-overlay"; # A helper for Rust + Nix + }; + + outputs = { self, nixpkgs, rust-overlay }: + let + overlays = [ + rust-overlay.overlays.default + (final: prev: { + rustToolchain = + let + rust = prev.rust-bin; + in + if builtins.pathExists ./rust-toolchain.toml then + rust.fromRustupToolchainFile ./rust-toolchain.toml + else if builtins.pathExists ./rust-toolchain then + rust.fromRustupToolchainFile ./rust-toolchain + else + rust.stable.latest.default; + }) + ]; + supportedSystems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ]; + forEachSupportedSystem = f: nixpkgs.lib.genAttrs supportedSystems (system: f { + pkgs = import nixpkgs { inherit overlays system; }; + }); + in + { + devShells = forEachSupportedSystem ({ pkgs }: + let + rustToolchain = pkgs.rust-bin.stable.latest.default.override { + extensions = [ "rust-src" ]; + targets = [ "wasm32-unknown-unknown" ]; + }; + in + { + default = pkgs.mkShell { + buildInputs = with pkgs; [ + rustToolchain + openssl + pkg-config + cargo-deny + cargo-edit + cargo-watch + rust-analyzer + wasm-pack + bun + nodejs + pnpm + # C/C++ Libraries + clang-tools + cmake + codespell + conan + cppcheck + doxygen + gtest + lcov + vcpkg + vcpkg-tool + ] + ++ (if system == "aarch64-darwin" then [ ] else [ gdb ]); + shellHook = '' + export LIBCLANG_PATH=${pkgs.libclang.lib}/lib/ + export LD_LIBRARY_PATH=${pkgs.openssl}/lib:$LD_LIBRARY_PATH + export CC_wasm32_unknown_unknown=${pkgs.llvmPackages.clang-unwrapped}/bin/clang + export CFLAGS_wasm32_unknown_unknown="-I ${pkgs.llvmPackages.libclang.lib}/lib/clang/include/" + ''; + }; + }); + }; +} diff --git a/packages/aiken-uplc/package.json b/packages/aiken-uplc/package.json new file mode 100644 index 00000000..fd7f61d1 --- /dev/null +++ b/packages/aiken-uplc/package.json @@ -0,0 +1,94 @@ +{ + "name": "@evolution-sdk/aiken-uplc", + "version": "0.0.0", + "description": "Aiken UPLC evaluator for Evolution SDK with WASM-based local script evaluation", + "type": "module", + "main": "./dist/index.js", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "sideEffects": [], + "tags": [ + "typescript", + "cardano", + "aiken", + "uplc", + "evaluator", + "wasm" + ], + "exports": { + ".": { + "types": "./src/index.node.ts", + "node": "./src/index.node.ts", + "browser": "./src/index.browser.ts", + "default": "./src/index.node.ts" + }, + "./package.json": "./package.json" + }, + "files": [ + "src/**/*.ts", + "dist/**/*.js", + "dist/**/*.js.map", + "dist/**/*.d.ts", + "dist/**/*.d.ts.map", + "dist/**/*.wasm" + ], + "scripts": { + "build:wasm": "rm -rf src/browser src/node && cd rust && wasm-pack build --target bundler --out-dir ../src/browser && wasm-pack build --target nodejs --out-dir ../src/node && pnpm clean-wasm", + "clean-wasm": "rm -f src/browser/.gitignore src/browser/package.json src/node/.gitignore src/node/package.json", + "build": "tsc -b tsconfig.build.json", + "dev": "tsc -b tsconfig.build.json --watch", + "type-check": "tsc --noEmit", + "lint": "eslint \"src/**/*.{ts,mjs}\"", + "clean": "rm -rf dist .turbo .tsbuildinfo" + }, + "devDependencies": { + "typescript": "^5.9.2" + }, + "dependencies": { + "effect": "^3.19.3" + }, + "peerDependencies": { + "@evolution-sdk/evolution": "workspace:*" + }, + "keywords": [ + "cardano", + "blockchain", + "aiken", + "uplc", + "evaluator", + "wasm", + "typescript", + "effect" + ], + "homepage": "https://github.com/IntersectMBO/evolution-sdk", + "repository": { + "type": "git", + "url": "git+https://github.com/IntersectMBO/evolution-sdk.git", + "directory": "packages/aiken-uplc" + }, + "bugs": { + "url": "https://github.com/IntersectMBO/evolution-sdk/issues" + }, + "license": "MIT", + "publishConfig": { + "access": "public", + "provenance": true, + "exports": { + ".": { + "node": { + "types": "./dist/index.node.d.ts", + "default": "./dist/index.node.js" + }, + "browser": { + "types": "./dist/index.browser.d.ts", + "default": "./dist/index.browser.js" + }, + "default": { + "types": "./dist/index.node.d.ts", + "default": "./dist/index.node.js" + } + }, + "./package.json": "./package.json" + } + } +} diff --git a/packages/aiken-uplc/rust-toolchain.toml b/packages/aiken-uplc/rust-toolchain.toml new file mode 100644 index 00000000..25046824 --- /dev/null +++ b/packages/aiken-uplc/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "stable" +targets = [ "wasm32-unknown-unknown" ] diff --git a/packages/aiken-uplc/rust/Cargo.lock b/packages/aiken-uplc/rust/Cargo.lock new file mode 100644 index 00000000..3c3231ad --- /dev/null +++ b/packages/aiken-uplc/rust/Cargo.lock @@ -0,0 +1,1095 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aiken-uplc" +version = "0.1.0" +dependencies = [ + "getrandom", + "js-sys", + "uplc", + "wasm-bindgen", + "wee_alloc", +] + +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base58" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6107fe1be6682a68940da878d9e9f5e90ca5745b3dec9fd1bb393c8777d4f581" + +[[package]] +name = "base64ct" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e050f626429857a27ddccb31e0aca21356bfa709c04041aefddac081a8f068a" + +[[package]] +name = "bech32" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "blst" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcdb4c7013139a150f9fc55d123186dbfaba0d912817466282c73ac49e71fb45" +dependencies = [ + "cc", + "glob", + "threadpool", + "zeroize", +] + +[[package]] +name = "bumpalo" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" + +[[package]] +name = "cc" +version = "1.2.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a0aeaff4ff1a90589618835a598e545176939b97874f7abc7851caa0618f203" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "cryptoxide" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "382ce8820a5bb815055d3553a610e8cb542b2d767bbacea99038afda96cd760d" + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "pkcs8", + "rand_core", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core", + "subtle", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff" + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "generic-array" +version = "0.14.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if 1.0.4", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core", + "subtle", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if 1.0.4", + "crunchy", + "zerocopy", +] + +[[package]] +name = "hamming" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65043da274378d68241eb9a8f8f8aa54e349136f7b8e12f63e3ef44043cc30e1" + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ee5b5339afb4c41626dde77b7a611bd4f2c202b897852b4bcf5d03eddc61010" + +[[package]] +name = "js-sys" +version = "0.3.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "k256" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" +dependencies = [ + "cfg-if 1.0.4", + "ecdsa", + "elliptic-curve", + "once_cell", + "sha2", + "signature", +] + +[[package]] +name = "libc" +version = "0.2.178" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "memory_units" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8452105ba047068f40ff7093dd1d9da90898e63dd61736462e9cdda6a90ad3c3" + +[[package]] +name = "miette" +version = "5.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59bb584eaeeab6bd0226ccf3509a69d7936d148cf3d036ad350abe35e8c6856e" +dependencies = [ + "miette-derive", + "once_cell", + "thiserror", + "unicode-width", +] + +[[package]] +name = "miette-derive" +version = "5.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49e7bc1560b95a3c4a25d03de42fe76ca718ab92d1a22a55b9b4cf67b3ae635c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "minicbor" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0452a60c1863c1f50b5f77cd295e8d2786849f35883f0b9e18e7e6e1b5691b0" +dependencies = [ + "half", + "minicbor-derive", +] + +[[package]] +name = "minicbor-derive" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd2209fff77f705b00c737016a48e73733d7fbccb8b007194db148f03561fb70" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "pallas-addresses" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18f5f4dd205316335bf8eef77227e01a8a00b1fd60503d807520e93dd0362d0e" +dependencies = [ + "base58", + "bech32", + "crc", + "cryptoxide", + "hex", + "pallas-codec", + "pallas-crypto", + "thiserror", +] + +[[package]] +name = "pallas-codec" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2737b05f0dbb6d197feeb26ef15d2567e54833184bd469f5655a0537da89fa" +dependencies = [ + "hex", + "minicbor", + "num-bigint", + "serde", + "thiserror", +] + +[[package]] +name = "pallas-crypto" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0368945cd093e550febe36aef085431b1611c2e9196297cd70f4b21a4add054c" +dependencies = [ + "cryptoxide", + "hex", + "pallas-codec", + "rand_core", + "serde", + "thiserror", + "zeroize", +] + +[[package]] +name = "pallas-primitives" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb2acde8875c43446194d387c60fe2d6a127e4f8384bef3dcabd5a04e9422429" +dependencies = [ + "base58", + "bech32", + "hex", + "log", + "pallas-codec", + "pallas-crypto", + "serde", + "serde_json", +] + +[[package]] +name = "pallas-traverse" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab64895a0d94fed1ef2d99dd37e480ed0483e91eb98dcd2f94cc614fb9575173" +dependencies = [ + "hex", + "itertools 0.13.0", + "pallas-addresses", + "pallas-codec", + "pallas-crypto", + "pallas-primitives", + "paste", + "serde", + "thiserror", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "peg" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9928cfca101b36ec5163e70049ee5368a8a1c3c6efc9ca9c5f9cc2f816152477" +dependencies = [ + "peg-macros", + "peg-runtime", +] + +[[package]] +name = "peg-macros" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6298ab04c202fa5b5d52ba03269fb7b74550b150323038878fe6c372d8280f71" +dependencies = [ + "peg-runtime", + "proc-macro2", + "quote", +] + +[[package]] +name = "peg-runtime" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "132dca9b868d927b35b5dd728167b2dee150eb1ad686008fc71ccb298b776fca" + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pretty" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83f3aa1e3ca87d3b124db7461265ac176b40c277f37e503eaa29c9c75c037846" +dependencies = [ + "arrayvec", + "log", + "typed-arena", + "unicode-segmentation", +] + +[[package]] +name = "proc-macro2" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + +[[package]] +name = "secp256k1" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4124a35fe33ae14259c490fd70fa199a32b9ce9502f2ee6bc4f81ec06fa65894" +dependencies = [ + "secp256k1-sys", +] + +[[package]] +name = "secp256k1-sys" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4473013577ec77b4ee3668179ef1186df3146e2cf2d927bd200974c6fe60fd99" +dependencies = [ + "cc", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.147" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6af14725505314343e673e9ecb7cd7e8a36aa9791eb936235a3567cc31447ae4" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if 1.0.4", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "threadpool" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" +dependencies = [ + "num_cpus", +] + +[[package]] +name = "typed-arena" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "uplc" +version = "1.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af10ae941c734f297a8ab1a08d79aa16e4216552bdc6b526dff8d91115c1eed9" +dependencies = [ + "bitvec", + "blst", + "cryptoxide", + "hamming", + "hex", + "indexmap", + "itertools 0.10.5", + "k256", + "miette", + "num-bigint", + "num-integer", + "num-traits", + "once_cell", + "pallas-addresses", + "pallas-codec", + "pallas-crypto", + "pallas-primitives", + "pallas-traverse", + "peg", + "pretty", + "secp256k1", + "serde", + "serde_json", + "strum", + "thiserror", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" +dependencies = [ + "cfg-if 1.0.4", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wee_alloc" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb3b5a6b2bb17cb6ad44a2e68a43e8d2722c997da10e928665c72ec6c0a0b8e" +dependencies = [ + "cfg-if 0.1.10", + "libc", + "memory_units", + "winapi", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "zerocopy" +version = "0.8.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0095ecd462946aa3927d9297b63ef82fb9a5316d7a37d134eeb36e58228615a" diff --git a/packages/aiken-uplc/rust/Cargo.toml b/packages/aiken-uplc/rust/Cargo.toml new file mode 100644 index 00000000..1ece1fd3 --- /dev/null +++ b/packages/aiken-uplc/rust/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "aiken-uplc" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +uplc = "1.1.21" +wasm-bindgen = "0.2.93" +js-sys = "0.3.70" +wee_alloc = "0.4.5" +getrandom = { version = "0.2", features = ["js"] } + +[profile.release] +opt-level = "z" +lto = true +codegen-units = 1 diff --git a/packages/aiken-uplc/rust/src/lib.rs b/packages/aiken-uplc/rust/src/lib.rs new file mode 100644 index 00000000..d415752e --- /dev/null +++ b/packages/aiken-uplc/rust/src/lib.rs @@ -0,0 +1,37 @@ +use js_sys; +use uplc::tx; +use wasm_bindgen::prelude::*; + +// Use `wee_alloc` as the global allocator. +#[global_allocator] +static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; + +#[wasm_bindgen] +pub fn eval_phase_two_raw( + tx_bytes: &[u8], + utxos_bytes_x: Vec, + utxos_bytes_y: Vec, + cost_mdls_bytes: &[u8], + initial_budget_n: u64, + initial_budget_d: u64, + slot_config_x: u64, + slot_config_y: u64, + slot_config_z: u32, +) -> Result, JsValue> { + let utxos_bytes = utxos_bytes_x + .into_iter() + .zip(utxos_bytes_y.into_iter()) + .map(|(x, y)| (x.to_vec(), y.to_vec())) + .collect::, Vec)>>(); + return tx::eval_phase_two_raw( + tx_bytes, + &utxos_bytes, + Some(cost_mdls_bytes), + (initial_budget_n, initial_budget_d), + (slot_config_x, slot_config_y, slot_config_z), + false, + |_| (), + ) + .map(|r| r.iter().map(|i| js_sys::Uint8Array::from(&i.0[..])).collect()) + .map_err(|e| e.to_string().into()); +} diff --git a/packages/aiken-uplc/src/Evaluator.ts b/packages/aiken-uplc/src/Evaluator.ts new file mode 100644 index 00000000..76950b77 --- /dev/null +++ b/packages/aiken-uplc/src/Evaluator.ts @@ -0,0 +1,208 @@ +/** + * Shared evaluator logic - platform agnostic + * + * @packageDocumentation + */ + +import * as Bytes from "@evolution-sdk/evolution/core/Bytes" +import * as CBOR from "@evolution-sdk/evolution/core/CBOR" +import * as Redeemer from "@evolution-sdk/evolution/core/Redeemer" +import * as Script from "@evolution-sdk/evolution/core/Script" +import * as ScriptRef from "@evolution-sdk/evolution/core/ScriptRef" +import * as Transaction from "@evolution-sdk/evolution/core/Transaction" +import * as TransactionInput from "@evolution-sdk/evolution/core/TransactionInput" +import * as TxOut from "@evolution-sdk/evolution/core/TxOut" +import type * as UTxO from "@evolution-sdk/evolution/core/UTxO" +import * as TransactionBuilder from "@evolution-sdk/evolution/sdk/builders/TransactionBuilder" +import type * as EvalRedeemer from "@evolution-sdk/evolution/sdk/EvalRedeemer" +import { Effect } from "effect" + +import type * as WasmLoader from "./WasmLoader.js" + +/** + * Parse Aiken UPLC error string into ScriptFailure array. + * + * Aiken errors come as strings like: + * - "Spend(0): validation failed: ..." + * - "Mint(1): ..." + * - "Withdraw(0): ..." + * - "Publish(0): ..." + */ +function parseAikenError(error: unknown): Array { + const failures: Array = [] + + const errorMessage = error instanceof Error ? error.message : String(error) + + // Pattern: Purpose(index): error message + // Examples: "Spend(0): validation failed", "Mint(1): budget exceeded" + const pattern = /\b(Spend|Mint|Withdraw|Publish|Reward|Cert)\s*\(\s*(\d+)\s*\)\s*:\s*(.+?)(?=\b(?:Spend|Mint|Withdraw|Publish|Reward|Cert)\s*\(|$)/gi + + let match + while ((match = pattern.exec(errorMessage)) !== null) { + const [, purposeRaw, indexStr, validationError] = match + const purpose = purposeRaw!.toLowerCase() + const index = parseInt(indexStr!, 10) + + // Normalize purpose names + const normalizedPurpose = + purpose === "reward" ? "withdraw" : + purpose === "cert" ? "publish" : + purpose + + failures.push({ + purpose: normalizedPurpose, + index, + validationError: validationError!.trim(), + traces: [] + }) + } + + // If no structured errors found, create a generic one + if (failures.length === 0 && errorMessage) { + failures.push({ + purpose: "unknown", + index: 0, + validationError: errorMessage, + traces: [] + }) + } + + return failures +} + +/** + * Convert UTxO to input CBOR bytes (transaction hash + index). + */ +function inputCBORFromUtxo(utxo: UTxO.UTxO): Uint8Array { + const txInput = new TransactionInput.TransactionInput({ + transactionId: utxo.transactionId, + index: utxo.index + }) + return TransactionInput.toCBORBytes(txInput) +} + +/** + * Convert UTxO output to CBOR bytes. + */ +function outputCBORFromUtxo(utxo: UTxO.UTxO): Uint8Array { + const scriptRef = utxo.scriptRef ? new ScriptRef.ScriptRef({ bytes: Script.toCBOR(utxo.scriptRef) }) : undefined + + const txOut = new TxOut.TransactionOutput({ + address: utxo.address, + assets: utxo.assets, + datumOption: utxo.datumOption, + scriptRef + }) + + return TxOut.toCBORBytes(txOut) +} + +/** + * Parse CBOR-encoded EvalRedeemer result from WASM. + * Uses the official Redeemer.fromCBORBytes which handles the 4-element tuple: + * [tag, index, data, [mem, steps]] + */ +function evalRedeemerFromCBOR(bytes: Uint8Array): EvalRedeemer.EvalRedeemer { + // Decode using official Redeemer module + const redeemer = Redeemer.fromCBORBytes(bytes, CBOR.CML_DEFAULT_OPTIONS) + + // Map Redeemer.RedeemerTag to EvalRedeemer tag format + // cert -> publish, reward -> withdraw (different naming conventions) + const tagMap: Record = { + spend: "spend", + mint: "mint", + cert: "publish", + reward: "withdraw", + vote: "vote", + propose: "propose" + } + + return { + redeemer_tag: tagMap[redeemer.tag], + redeemer_index: Number(redeemer.index), + ex_units: { + mem: Number(redeemer.exUnits.mem), + steps: Number(redeemer.exUnits.steps) + } + } +} + +/** + * Create Aiken evaluator - accepts WASM module + */ +export function makeEvaluator(wasmModule: WasmLoader.WasmModule): TransactionBuilder.Evaluator { + return { + evaluate: ( + tx: Transaction.Transaction, + additionalUtxos: ReadonlyArray | undefined, + context: TransactionBuilder.EvaluationContext + ) => + Effect.gen(function* () { + yield* Effect.logDebug("[Aiken UPLC] Starting evaluation") + + // Serialize transaction to CBOR bytes + const txBytes = Transaction.toCBORBytes(tx) + + yield* Effect.logDebug(`[Aiken UPLC] Transaction CBOR bytes: ${txBytes.length}`) + + const utxos = additionalUtxos ?? [] + yield* Effect.logDebug(`[Aiken UPLC] Additional UTxOs: ${utxos.length}`) + + // Serialize UTxOs to CBOR arrays + const utxosX = utxos.map(inputCBORFromUtxo) + const utxosY = utxos.map(outputCBORFromUtxo) + + const { slotLength, zeroSlot, zeroTime } = context.slotConfig + + yield* Effect.logDebug( + `[Aiken UPLC] Slot config - zeroTime: ${zeroTime}, zeroSlot: ${zeroSlot}, slotLength: ${slotLength}` + ) + yield* Effect.logDebug(`[Aiken UPLC] Cost models CBOR length: ${context.costModels.length} bytes`) + yield* Effect.logDebug(`[Aiken UPLC] Cost models hex: ${Bytes.toHex(context.costModels)}`) + yield* Effect.logDebug( + `[Aiken UPLC] Max execution - steps: ${context.maxTxExSteps}, mem: ${context.maxTxExMem}` + ) + + // Note: Some protocol parameters (especially cost models) may contain values + // that overflow i64 during CBOR decoding in the WASM evaluator. + // This is a known limitation when cost model parameters are set to large values (e.g., 2^63). + yield* Effect.logDebug("[Aiken UPLC] Calling eval_phase_two_raw...") + const resultBytes = yield* Effect.try({ + try: () => + wasmModule.eval_phase_two_raw( + txBytes, + utxosX, + utxosY, + context.costModels, + context.maxTxExSteps, + context.maxTxExMem, + BigInt(zeroTime), + BigInt(zeroSlot), + slotLength + ), + catch: (error) => { + const message = error instanceof Error ? error.message : "UPLC evaluation failed" + const failures = parseAikenError(error) + return new TransactionBuilder.EvaluationError({ + cause: error, + message, + failures + }) + } + }) + + yield* Effect.logDebug(`[Aiken UPLC] Evaluation successful - ${resultBytes.length} redeemer(s) returned`) + + const evalRedeemers = yield* Effect.try({ + try: () => resultBytes.map(evalRedeemerFromCBOR), + catch: (error) => + new TransactionBuilder.EvaluationError({ + cause: error, + message: error instanceof Error ? error.message : "Failed to parse evaluation results" + }) + }) + + return evalRedeemers + }) + } +} diff --git a/packages/aiken-uplc/src/WasmLoader.ts b/packages/aiken-uplc/src/WasmLoader.ts new file mode 100644 index 00000000..668b28c5 --- /dev/null +++ b/packages/aiken-uplc/src/WasmLoader.ts @@ -0,0 +1,22 @@ +/** + * Type definitions for WASM module interface + * + * @packageDocumentation + */ + +/** + * WASM module interface for Aiken UPLC evaluator + */ +export interface WasmModule { + eval_phase_two_raw( + tx_bytes: Uint8Array, + utxos_x: Array, + utxos_y: Array, + cost_mdls: Uint8Array, + initial_budget_n: bigint, + initial_budget_d: bigint, + slot_x: bigint, + slot_y: bigint, + slot_z: number + ): Array +} diff --git a/packages/aiken-uplc/src/browser/aiken_uplc.d.ts b/packages/aiken-uplc/src/browser/aiken_uplc.d.ts new file mode 100644 index 00000000..57ace530 --- /dev/null +++ b/packages/aiken-uplc/src/browser/aiken_uplc.d.ts @@ -0,0 +1,4 @@ +/* tslint:disable */ +/* eslint-disable */ + +export function eval_phase_two_raw(tx_bytes: Uint8Array, utxos_bytes_x: Uint8Array[], utxos_bytes_y: Uint8Array[], cost_mdls_bytes: Uint8Array, initial_budget_n: bigint, initial_budget_d: bigint, slot_config_x: bigint, slot_config_y: bigint, slot_config_z: number): Uint8Array[]; diff --git a/packages/aiken-uplc/src/browser/aiken_uplc.js b/packages/aiken-uplc/src/browser/aiken_uplc.js new file mode 100644 index 00000000..7ff79ff1 --- /dev/null +++ b/packages/aiken-uplc/src/browser/aiken_uplc.js @@ -0,0 +1,5 @@ +import * as wasm from "./aiken_uplc_bg.wasm"; +export * from "./aiken_uplc_bg.js"; +import { __wbg_set_wasm } from "./aiken_uplc_bg.js"; +__wbg_set_wasm(wasm); +wasm.__wbindgen_start(); diff --git a/packages/aiken-uplc/src/browser/aiken_uplc_bg.js b/packages/aiken-uplc/src/browser/aiken_uplc_bg.js new file mode 100644 index 00000000..114c3e89 --- /dev/null +++ b/packages/aiken-uplc/src/browser/aiken_uplc_bg.js @@ -0,0 +1,150 @@ +let wasm; +export function __wbg_set_wasm(val) { + wasm = val; +} + +function addToExternrefTable0(obj) { + const idx = wasm.__externref_table_alloc(); + wasm.__wbindgen_externrefs.set(idx, obj); + return idx; +} + +function getArrayJsValueFromWasm0(ptr, len) { + ptr = ptr >>> 0; + const mem = getDataViewMemory0(); + const result = []; + for (let i = ptr; i < ptr + 4 * len; i += 4) { + result.push(wasm.__wbindgen_externrefs.get(mem.getUint32(i, true))); + } + wasm.__externref_drop_slice(ptr, len); + return result; +} + +function getArrayU8FromWasm0(ptr, len) { + ptr = ptr >>> 0; + return getUint8ArrayMemory0().subarray(ptr / 1, ptr / 1 + len); +} + +let cachedDataViewMemory0 = null; +function getDataViewMemory0() { + if (cachedDataViewMemory0 === null || cachedDataViewMemory0.buffer.detached === true || (cachedDataViewMemory0.buffer.detached === undefined && cachedDataViewMemory0.buffer !== wasm.memory.buffer)) { + cachedDataViewMemory0 = new DataView(wasm.memory.buffer); + } + return cachedDataViewMemory0; +} + +function getStringFromWasm0(ptr, len) { + ptr = ptr >>> 0; + return decodeText(ptr, len); +} + +let cachedUint8ArrayMemory0 = null; +function getUint8ArrayMemory0() { + if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) { + cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer); + } + return cachedUint8ArrayMemory0; +} + +function passArray8ToWasm0(arg, malloc) { + const ptr = malloc(arg.length * 1, 1) >>> 0; + getUint8ArrayMemory0().set(arg, ptr / 1); + WASM_VECTOR_LEN = arg.length; + return ptr; +} + +function passArrayJsValueToWasm0(array, malloc) { + const ptr = malloc(array.length * 4, 4) >>> 0; + for (let i = 0; i < array.length; i++) { + const add = addToExternrefTable0(array[i]); + getDataViewMemory0().setUint32(ptr + 4 * i, add, true); + } + WASM_VECTOR_LEN = array.length; + return ptr; +} + +function takeFromExternrefTable0(idx) { + const value = wasm.__wbindgen_externrefs.get(idx); + wasm.__externref_table_dealloc(idx); + return value; +} + +let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }); +cachedTextDecoder.decode(); +const MAX_SAFARI_DECODE_BYTES = 2146435072; +let numBytesDecoded = 0; +function decodeText(ptr, len) { + numBytesDecoded += len; + if (numBytesDecoded >= MAX_SAFARI_DECODE_BYTES) { + cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }); + cachedTextDecoder.decode(); + numBytesDecoded = len; + } + return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len)); +} + +let WASM_VECTOR_LEN = 0; + +/** + * @param {Uint8Array} tx_bytes + * @param {Uint8Array[]} utxos_bytes_x + * @param {Uint8Array[]} utxos_bytes_y + * @param {Uint8Array} cost_mdls_bytes + * @param {bigint} initial_budget_n + * @param {bigint} initial_budget_d + * @param {bigint} slot_config_x + * @param {bigint} slot_config_y + * @param {number} slot_config_z + * @returns {Uint8Array[]} + */ +export function eval_phase_two_raw(tx_bytes, utxos_bytes_x, utxos_bytes_y, cost_mdls_bytes, initial_budget_n, initial_budget_d, slot_config_x, slot_config_y, slot_config_z) { + const ptr0 = passArray8ToWasm0(tx_bytes, wasm.__wbindgen_malloc); + const len0 = WASM_VECTOR_LEN; + const ptr1 = passArrayJsValueToWasm0(utxos_bytes_x, wasm.__wbindgen_malloc); + const len1 = WASM_VECTOR_LEN; + const ptr2 = passArrayJsValueToWasm0(utxos_bytes_y, wasm.__wbindgen_malloc); + const len2 = WASM_VECTOR_LEN; + const ptr3 = passArray8ToWasm0(cost_mdls_bytes, wasm.__wbindgen_malloc); + const len3 = WASM_VECTOR_LEN; + const ret = wasm.eval_phase_two_raw(ptr0, len0, ptr1, len1, ptr2, len2, ptr3, len3, initial_budget_n, initial_budget_d, slot_config_x, slot_config_y, slot_config_z); + if (ret[3]) { + throw takeFromExternrefTable0(ret[2]); + } + var v5 = getArrayJsValueFromWasm0(ret[0], ret[1]).slice(); + wasm.__wbindgen_free(ret[0], ret[1] * 4, 4); + return v5; +} + +export function __wbg___wbindgen_throw_dd24417ed36fc46e(arg0, arg1) { + throw new Error(getStringFromWasm0(arg0, arg1)); +}; + +export function __wbg_length_22ac23eaec9d8053(arg0) { + const ret = arg0.length; + return ret; +}; + +export function __wbg_new_from_slice_f9c22b9153b26992(arg0, arg1) { + const ret = new Uint8Array(getArrayU8FromWasm0(arg0, arg1)); + return ret; +}; + +export function __wbg_prototypesetcall_dfe9b766cdc1f1fd(arg0, arg1, arg2) { + Uint8Array.prototype.set.call(getArrayU8FromWasm0(arg0, arg1), arg2); +}; + +export function __wbindgen_cast_2241b6af4c4b2941(arg0, arg1) { + // Cast intrinsic for `Ref(String) -> Externref`. + const ret = getStringFromWasm0(arg0, arg1); + return ret; +}; + +export function __wbindgen_init_externref_table() { + const table = wasm.__wbindgen_externrefs; + const offset = table.grow(4); + table.set(0, undefined); + table.set(offset + 0, undefined); + table.set(offset + 1, null); + table.set(offset + 2, true); + table.set(offset + 3, false); +}; diff --git a/packages/aiken-uplc/src/browser/aiken_uplc_bg.wasm b/packages/aiken-uplc/src/browser/aiken_uplc_bg.wasm new file mode 100644 index 00000000..340baa01 Binary files /dev/null and b/packages/aiken-uplc/src/browser/aiken_uplc_bg.wasm differ diff --git a/packages/aiken-uplc/src/browser/aiken_uplc_bg.wasm.d.ts b/packages/aiken-uplc/src/browser/aiken_uplc_bg.wasm.d.ts new file mode 100644 index 00000000..49f6138d --- /dev/null +++ b/packages/aiken-uplc/src/browser/aiken_uplc_bg.wasm.d.ts @@ -0,0 +1,11 @@ +/* tslint:disable */ +/* eslint-disable */ +export const memory: WebAssembly.Memory; +export const eval_phase_two_raw: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: bigint, j: bigint, k: bigint, l: bigint, m: number) => [number, number, number, number]; +export const __wbindgen_externrefs: WebAssembly.Table; +export const __wbindgen_malloc: (a: number, b: number) => number; +export const __externref_table_alloc: () => number; +export const __externref_table_dealloc: (a: number) => void; +export const __externref_drop_slice: (a: number, b: number) => void; +export const __wbindgen_free: (a: number, b: number, c: number) => void; +export const __wbindgen_start: () => void; diff --git a/packages/aiken-uplc/src/index.browser.ts b/packages/aiken-uplc/src/index.browser.ts new file mode 100644 index 00000000..beb03c91 --- /dev/null +++ b/packages/aiken-uplc/src/index.browser.ts @@ -0,0 +1,39 @@ +/** + * Browser entry point - uses bundler WASM target + * + * @packageDocumentation + */ + +import * as wasmModule from "./browser/aiken_uplc.js" +import { makeEvaluator } from "./Evaluator.js" + +/** + * Create an Aiken UPLC evaluator for Evolution SDK (Browser). + * + * This evaluator provides local UPLC script evaluation using Aiken's evaluator + * compiled to WASM, enabling offline transaction building and testing without + * requiring a provider connection. + * + * **Benefits:** + * - No network dependency (works offline) + * - Privacy (transaction never leaves local environment) + * - Performance (no network latency) + * - Deterministic evaluation for testing + * + * @example + * ```typescript + * import { makeTxBuilder } from "@evolution-sdk/evolution" + * import { createAikenEvaluator } from "@evolution-sdk/aiken-uplc" + * + * const builder = makeTxBuilder({ wallet, provider }) + * + * const tx = await builder + * .collectFrom([scriptUtxo], redeemer) + * .attachScript({ script: validatorScript }) + * .payToAddress({ address: recipientAddr, assets }) + * .build({ + * evaluator: createAikenEvaluator + * }) + * ``` + */ +export const createAikenEvaluator = makeEvaluator(wasmModule) diff --git a/packages/aiken-uplc/src/index.node.ts b/packages/aiken-uplc/src/index.node.ts new file mode 100644 index 00000000..c9664723 --- /dev/null +++ b/packages/aiken-uplc/src/index.node.ts @@ -0,0 +1,39 @@ +/** + * Node.js entry point - uses Node.js WASM target + * + * @packageDocumentation + */ + +import { makeEvaluator } from "./Evaluator.js" +import * as wasmModule from "./node/aiken_uplc.js" + +/** + * Create an Aiken UPLC evaluator for Evolution SDK (Node.js). + * + * This evaluator provides local UPLC script evaluation using Aiken's evaluator + * compiled to WASM, enabling offline transaction building and testing without + * requiring a provider connection. + * + * **Benefits:** + * - No network dependency (works offline) + * - Privacy (transaction never leaves local environment) + * - Performance (no network latency) + * - Deterministic evaluation for testing + * + * @example + * ```typescript + * import { makeTxBuilder } from "@evolution-sdk/evolution" + * import { createAikenEvaluator } from "@evolution-sdk/aiken-uplc" + * + * const builder = makeTxBuilder({ wallet, provider }) + * + * const tx = await builder + * .collectFrom([scriptUtxo], redeemer) + * .attachScript({ script: validatorScript }) + * .payToAddress({ address: recipientAddr, assets }) + * .build({ + * evaluator: createAikenEvaluator + * }) + * ``` + */ +export const createAikenEvaluator = makeEvaluator(wasmModule) diff --git a/packages/aiken-uplc/src/node/aiken_uplc.d.ts b/packages/aiken-uplc/src/node/aiken_uplc.d.ts new file mode 100644 index 00000000..57ace530 --- /dev/null +++ b/packages/aiken-uplc/src/node/aiken_uplc.d.ts @@ -0,0 +1,4 @@ +/* tslint:disable */ +/* eslint-disable */ + +export function eval_phase_two_raw(tx_bytes: Uint8Array, utxos_bytes_x: Uint8Array[], utxos_bytes_y: Uint8Array[], cost_mdls_bytes: Uint8Array, initial_budget_n: bigint, initial_budget_d: bigint, slot_config_x: bigint, slot_config_y: bigint, slot_config_z: number): Uint8Array[]; diff --git a/packages/aiken-uplc/src/node/aiken_uplc.js b/packages/aiken-uplc/src/node/aiken_uplc.js new file mode 100644 index 00000000..a498b7ae --- /dev/null +++ b/packages/aiken-uplc/src/node/aiken_uplc.js @@ -0,0 +1,149 @@ + +let imports = {}; +imports['__wbindgen_placeholder__'] = module.exports; + +function addToExternrefTable0(obj) { + const idx = wasm.__externref_table_alloc(); + wasm.__wbindgen_externrefs.set(idx, obj); + return idx; +} + +function getArrayJsValueFromWasm0(ptr, len) { + ptr = ptr >>> 0; + const mem = getDataViewMemory0(); + const result = []; + for (let i = ptr; i < ptr + 4 * len; i += 4) { + result.push(wasm.__wbindgen_externrefs.get(mem.getUint32(i, true))); + } + wasm.__externref_drop_slice(ptr, len); + return result; +} + +function getArrayU8FromWasm0(ptr, len) { + ptr = ptr >>> 0; + return getUint8ArrayMemory0().subarray(ptr / 1, ptr / 1 + len); +} + +let cachedDataViewMemory0 = null; +function getDataViewMemory0() { + if (cachedDataViewMemory0 === null || cachedDataViewMemory0.buffer.detached === true || (cachedDataViewMemory0.buffer.detached === undefined && cachedDataViewMemory0.buffer !== wasm.memory.buffer)) { + cachedDataViewMemory0 = new DataView(wasm.memory.buffer); + } + return cachedDataViewMemory0; +} + +function getStringFromWasm0(ptr, len) { + ptr = ptr >>> 0; + return decodeText(ptr, len); +} + +let cachedUint8ArrayMemory0 = null; +function getUint8ArrayMemory0() { + if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) { + cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer); + } + return cachedUint8ArrayMemory0; +} + +function passArray8ToWasm0(arg, malloc) { + const ptr = malloc(arg.length * 1, 1) >>> 0; + getUint8ArrayMemory0().set(arg, ptr / 1); + WASM_VECTOR_LEN = arg.length; + return ptr; +} + +function passArrayJsValueToWasm0(array, malloc) { + const ptr = malloc(array.length * 4, 4) >>> 0; + for (let i = 0; i < array.length; i++) { + const add = addToExternrefTable0(array[i]); + getDataViewMemory0().setUint32(ptr + 4 * i, add, true); + } + WASM_VECTOR_LEN = array.length; + return ptr; +} + +function takeFromExternrefTable0(idx) { + const value = wasm.__wbindgen_externrefs.get(idx); + wasm.__externref_table_dealloc(idx); + return value; +} + +let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }); +cachedTextDecoder.decode(); +function decodeText(ptr, len) { + return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len)); +} + +let WASM_VECTOR_LEN = 0; + +/** + * @param {Uint8Array} tx_bytes + * @param {Uint8Array[]} utxos_bytes_x + * @param {Uint8Array[]} utxos_bytes_y + * @param {Uint8Array} cost_mdls_bytes + * @param {bigint} initial_budget_n + * @param {bigint} initial_budget_d + * @param {bigint} slot_config_x + * @param {bigint} slot_config_y + * @param {number} slot_config_z + * @returns {Uint8Array[]} + */ +function eval_phase_two_raw(tx_bytes, utxos_bytes_x, utxos_bytes_y, cost_mdls_bytes, initial_budget_n, initial_budget_d, slot_config_x, slot_config_y, slot_config_z) { + const ptr0 = passArray8ToWasm0(tx_bytes, wasm.__wbindgen_malloc); + const len0 = WASM_VECTOR_LEN; + const ptr1 = passArrayJsValueToWasm0(utxos_bytes_x, wasm.__wbindgen_malloc); + const len1 = WASM_VECTOR_LEN; + const ptr2 = passArrayJsValueToWasm0(utxos_bytes_y, wasm.__wbindgen_malloc); + const len2 = WASM_VECTOR_LEN; + const ptr3 = passArray8ToWasm0(cost_mdls_bytes, wasm.__wbindgen_malloc); + const len3 = WASM_VECTOR_LEN; + const ret = wasm.eval_phase_two_raw(ptr0, len0, ptr1, len1, ptr2, len2, ptr3, len3, initial_budget_n, initial_budget_d, slot_config_x, slot_config_y, slot_config_z); + if (ret[3]) { + throw takeFromExternrefTable0(ret[2]); + } + var v5 = getArrayJsValueFromWasm0(ret[0], ret[1]).slice(); + wasm.__wbindgen_free(ret[0], ret[1] * 4, 4); + return v5; +} +exports.eval_phase_two_raw = eval_phase_two_raw; + +exports.__wbg___wbindgen_throw_dd24417ed36fc46e = function(arg0, arg1) { + throw new Error(getStringFromWasm0(arg0, arg1)); +}; + +exports.__wbg_length_22ac23eaec9d8053 = function(arg0) { + const ret = arg0.length; + return ret; +}; + +exports.__wbg_new_from_slice_f9c22b9153b26992 = function(arg0, arg1) { + const ret = new Uint8Array(getArrayU8FromWasm0(arg0, arg1)); + return ret; +}; + +exports.__wbg_prototypesetcall_dfe9b766cdc1f1fd = function(arg0, arg1, arg2) { + Uint8Array.prototype.set.call(getArrayU8FromWasm0(arg0, arg1), arg2); +}; + +exports.__wbindgen_cast_2241b6af4c4b2941 = function(arg0, arg1) { + // Cast intrinsic for `Ref(String) -> Externref`. + const ret = getStringFromWasm0(arg0, arg1); + return ret; +}; + +exports.__wbindgen_init_externref_table = function() { + const table = wasm.__wbindgen_externrefs; + const offset = table.grow(4); + table.set(0, undefined); + table.set(offset + 0, undefined); + table.set(offset + 1, null); + table.set(offset + 2, true); + table.set(offset + 3, false); +}; + +const wasmPath = `${__dirname}/aiken_uplc_bg.wasm`; +const wasmBytes = require('fs').readFileSync(wasmPath); +const wasmModule = new WebAssembly.Module(wasmBytes); +const wasm = exports.__wasm = new WebAssembly.Instance(wasmModule, imports).exports; + +wasm.__wbindgen_start(); diff --git a/packages/aiken-uplc/src/node/aiken_uplc_bg.wasm b/packages/aiken-uplc/src/node/aiken_uplc_bg.wasm new file mode 100644 index 00000000..ecb53199 Binary files /dev/null and b/packages/aiken-uplc/src/node/aiken_uplc_bg.wasm differ diff --git a/packages/aiken-uplc/src/node/aiken_uplc_bg.wasm.d.ts b/packages/aiken-uplc/src/node/aiken_uplc_bg.wasm.d.ts new file mode 100644 index 00000000..49f6138d --- /dev/null +++ b/packages/aiken-uplc/src/node/aiken_uplc_bg.wasm.d.ts @@ -0,0 +1,11 @@ +/* tslint:disable */ +/* eslint-disable */ +export const memory: WebAssembly.Memory; +export const eval_phase_two_raw: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: bigint, j: bigint, k: bigint, l: bigint, m: number) => [number, number, number, number]; +export const __wbindgen_externrefs: WebAssembly.Table; +export const __wbindgen_malloc: (a: number, b: number) => number; +export const __externref_table_alloc: () => number; +export const __externref_table_dealloc: (a: number) => void; +export const __externref_drop_slice: (a: number, b: number) => void; +export const __wbindgen_free: (a: number, b: number, c: number) => void; +export const __wbindgen_start: () => void; diff --git a/packages/aiken-uplc/tsconfig.build.json b/packages/aiken-uplc/tsconfig.build.json new file mode 100644 index 00000000..02c325c6 --- /dev/null +++ b/packages/aiken-uplc/tsconfig.build.json @@ -0,0 +1,10 @@ +{ + "$schema": "http://json.schemastore.org/tsconfig", + "extends": "./tsconfig.src.json", + "compilerOptions": { + "tsBuildInfoFile": ".tsbuildinfo/build.tsbuildinfo", + "outDir": "dist", + "types": ["node"], + "stripInternal": true + } +} diff --git a/packages/aiken-uplc/tsconfig.json b/packages/aiken-uplc/tsconfig.json new file mode 100644 index 00000000..6e773dfe --- /dev/null +++ b/packages/aiken-uplc/tsconfig.json @@ -0,0 +1,8 @@ +{ + "$schema": "http://json.schemastore.org/tsconfig", + "files": [], + "references": [ + { "path": "tsconfig.src.json" }, + { "path": "tsconfig.test.json" } + ] +} diff --git a/packages/aiken-uplc/tsconfig.src.json b/packages/aiken-uplc/tsconfig.src.json new file mode 100644 index 00000000..0f916616 --- /dev/null +++ b/packages/aiken-uplc/tsconfig.src.json @@ -0,0 +1,10 @@ +{ + "$schema": "http://json.schemastore.org/tsconfig", + "extends": "../../tsconfig.base.json", + "include": ["src"], + "compilerOptions": { + "tsBuildInfoFile": ".tsbuildinfo/src.tsbuildinfo", + "outDir": ".tsbuildinfo/src", + "rootDir": "src" + } +} diff --git a/packages/aiken-uplc/tsconfig.test.json b/packages/aiken-uplc/tsconfig.test.json new file mode 100644 index 00000000..d4aefc3f --- /dev/null +++ b/packages/aiken-uplc/tsconfig.test.json @@ -0,0 +1,16 @@ +{ + "$schema": "http://json.schemastore.org/tsconfig", + "extends": "../../tsconfig.base.json", + "include": ["test/**/*", "**/*.test.ts", "**/*.spec.ts"], + "references": [{ "path": "tsconfig.src.json" }], + "compilerOptions": { + "tsBuildInfoFile": ".tsbuildinfo/test.tsbuildinfo", + "outDir": ".tsbuildinfo/test", + "noEmit": true, + "baseUrl": ".", + "paths": { + "@evolution-sdk/aiken-uplc": ["src/index.node.ts"], + "@evolution-sdk/aiken-uplc/*": ["src/*/index.ts", "src/*.ts"] + } + } +} diff --git a/packages/evolution-devnet/package.json b/packages/evolution-devnet/package.json index 6666bf23..b14b9744 100644 --- a/packages/evolution-devnet/package.json +++ b/packages/evolution-devnet/package.json @@ -47,6 +47,7 @@ "dockerode": "^4.0.7" }, "peerDependencies": { + "@evolution-sdk/aiken-uplc": "workspace:*", "@evolution-sdk/evolution": "workspace:*", "@effect/platform": "^0.90.10", "@effect/platform-node": "^0.96.1", diff --git a/packages/evolution-devnet/test/TxBuilder.Compose.test.ts b/packages/evolution-devnet/test/TxBuilder.Compose.test.ts new file mode 100644 index 00000000..93397d24 --- /dev/null +++ b/packages/evolution-devnet/test/TxBuilder.Compose.test.ts @@ -0,0 +1,343 @@ +/** + * Devnet tests for TxBuilder compose operation. + * + * Tests the compose operation which merges multiple transaction builders + * into a single transaction, enabling modular and reusable transaction patterns. + */ + +import { afterAll, beforeAll, describe, expect, it } from "@effect/vitest" +import * as Cluster from "@evolution-sdk/devnet/Cluster" +import * as Config from "@evolution-sdk/devnet/Config" +import * as Genesis from "@evolution-sdk/devnet/Genesis" +import { Core } from "@evolution-sdk/evolution" +import * as Address from "@evolution-sdk/evolution/core/Address" +import { createClient } from "@evolution-sdk/evolution/sdk/client/ClientImpl" + +// Alias for readability +const Time = Core.Time + +describe("TxBuilder compose (Devnet Submit)", () => { + let devnetCluster: Cluster.Cluster | undefined + let genesisConfig: Config.ShelleyGenesis + let genesisUtxos: ReadonlyArray = [] + + const TEST_MNEMONIC = + "test test test test test test test test test test test test test test test test test test test test test test test sauce" + + const createTestClient = (accountIndex: number = 0) => { + if (!devnetCluster) throw new Error("Cluster not initialized") + const slotConfig = Cluster.getSlotConfig(devnetCluster) + return createClient({ + network: 0, + slotConfig, + provider: { + type: "kupmios", + kupoUrl: "http://localhost:1451", + ogmiosUrl: "http://localhost:1346" + }, + wallet: { + type: "seed", + mnemonic: TEST_MNEMONIC, + accountIndex, + addressType: "Base" + } + }) + } + + beforeAll(async () => { + const tempClient = createClient({ + network: 0, + wallet: { type: "seed", mnemonic: TEST_MNEMONIC, accountIndex: 0, addressType: "Base" } + }) + + const testAddress = await tempClient.address() + const testAddressHex = Address.toHex(testAddress) + + genesisConfig = { + ...Config.DEFAULT_SHELLEY_GENESIS, + slotLength: 0.02, + epochLength: 50, + activeSlotsCoeff: 1.0, + initialFunds: { [testAddressHex]: 500_000_000_000 } + } + + genesisUtxos = await Genesis.calculateUtxosFromConfig(genesisConfig) + + devnetCluster = await Cluster.make({ + clusterName: "compose-test", + ports: { node: 6009, submit: 9010 }, + shelleyGenesis: genesisConfig, + kupo: { enabled: true, port: 1451, logLevel: "Info" }, + ogmios: { enabled: true, port: 1346, logLevel: "info" } + }) + + await Cluster.start(devnetCluster) + await new Promise((resolve) => setTimeout(resolve, 3_000)) + }, 180_000) + + afterAll(async () => { + if (devnetCluster) { + await Cluster.stop(devnetCluster) + await Cluster.remove(devnetCluster) + } + }, 60_000) + + it("should compose payment with validity constraints", { timeout: 60_000 }, async () => { + const client = createTestClient(0) + const myAddress = await client.address() + + // Create a payment builder + const paymentBuilder = client.newTx().payToAddress({ + address: myAddress, + assets: Core.Assets.fromLovelace(5_000_000n) + }) + + // Create a validity builder + const validityBuilder = client.newTx().setValidity({ + to: Time.now() + 300_000n + }) + + // Compose payment and validity together + const signBuilder = await client + .newTx() + .compose(paymentBuilder) + .compose(validityBuilder) + .build({ availableUtxos: [...genesisUtxos] }) + + const tx = await signBuilder.toTransaction() + + // Verify validity interval is set + expect(tx.body.ttl).toBeDefined() + expect(tx.body.ttl).toBeGreaterThan(0n) + + // Submit and confirm + const submitBuilder = await signBuilder.sign() + const txHash = await submitBuilder.submit() + expect(txHash.length).toBe(64) + + const confirmed = await client.awaitTx(txHash, 1000) + expect(confirmed).toBe(true) + }) + + it("should compose multiple payment builders to different addresses", { timeout: 60_000 }, async () => { + const client1 = createTestClient(0) + const client2 = createTestClient(1) + + const address1 = await client1.address() + const address2 = await client2.address() + + // Create separate payment builders for different addresses + const payment1 = client1.newTx().payToAddress({ + address: address1, + assets: Core.Assets.fromLovelace(3_000_000n) + }) + + const payment2 = client1.newTx().payToAddress({ + address: address2, + assets: Core.Assets.fromLovelace(2_000_000n) + }) + + const payment3 = client1.newTx().payToAddress({ + address: address1, + assets: Core.Assets.fromLovelace(4_000_000n) + }) + + // Compose all payments into single transaction + const signBuilder = await client1 + .newTx() + .compose(payment1) + .compose(payment2) + .compose(payment3) + .build() + + const tx = await signBuilder.toTransaction() + + // Should have at least 3 payment outputs (plus change) + expect(tx.body.outputs.length).toBeGreaterThanOrEqual(3) + + // Submit and confirm + const submitBuilder = await signBuilder.sign() + const txHash = await submitBuilder.submit() + expect(txHash.length).toBe(64) + + const confirmed = await client1.awaitTx(txHash, 1000) + expect(confirmed).toBe(true) + }) + + it("should compose builder with addSigner + metadata + payment", { timeout: 60_000 }, async () => { + const client = createTestClient(0) + const myAddress = await client.address() + + // Extract payment credential + const paymentCredential = myAddress.paymentCredential + if (paymentCredential._tag !== "KeyHash") { + throw new Error("Expected KeyHash credential") + } + + // Create modular builders + const signerBuilder = client.newTx().addSigner({ keyHash: paymentCredential }) + + const metadataBuilder = client.newTx().attachMetadata({ + label: 674n, + metadata: "Multi-sig transaction" + }) + + const paymentBuilder = client.newTx().payToAddress({ + address: myAddress, + assets: Core.Assets.fromLovelace(6_000_000n) + }) + + // Compose all together + const signBuilder = await client + .newTx() + .compose(signerBuilder) + .compose(metadataBuilder) + .compose(paymentBuilder) + .build() + + const tx = await signBuilder.toTransaction() + + // Verify all components + expect(tx.body.requiredSigners?.length).toBe(1) + expect(tx.auxiliaryData).toBeDefined() + expect(tx.body.outputs.length).toBeGreaterThanOrEqual(1) + + // Submit and confirm + const submitBuilder = await signBuilder.sign() + const txHash = await submitBuilder.submit() + expect(txHash.length).toBe(64) + + const confirmed = await client.awaitTx(txHash, 1000) + expect(confirmed).toBe(true) + }) + + it("should compose stake registration with payment and metadata", { timeout: 90_000 }, async () => { + const client = createTestClient(0) + const myAddress = await client.address() + + // Get stake credential from address + if (!("stakingCredential" in myAddress) || !myAddress.stakingCredential) { + throw new Error("Expected BaseAddress with stakingCredential") + } + + const stakeCredential = myAddress.stakingCredential + + // Create separate builders for each operation + const stakeBuilder = client.newTx().registerStake({ stakeCredential }) + + const paymentBuilder = client.newTx().payToAddress({ + address: myAddress, + assets: Core.Assets.fromLovelace(10_000_000n) + }) + + const metadataBuilder = client.newTx().attachMetadata({ + label: 674n, + metadata: "Stake registration transaction" + }) + + // Compose all operations together + const signBuilder = await client + .newTx() + .compose(stakeBuilder) + .compose(paymentBuilder) + .compose(metadataBuilder) + .build() + + const tx = await signBuilder.toTransaction() + + // Verify all components + expect(tx.body.certificates).toBeDefined() + expect(tx.body.certificates?.length).toBe(1) + expect(tx.body.outputs.length).toBeGreaterThanOrEqual(1) + expect(tx.auxiliaryData).toBeDefined() + + // Submit and confirm + const submitBuilder = await signBuilder.sign() + const txHash = await submitBuilder.submit() + expect(txHash.length).toBe(64) + + const confirmed = await client.awaitTx(txHash, 1000) + expect(confirmed).toBe(true) + }) + + it("should verify getPrograms returns accumulated operations", { timeout: 30_000 }, async () => { + const client = createTestClient(0) + const myAddress = await client.address() + + // Build a transaction with multiple operations + const builder = client + .newTx() + .payToAddress({ + address: myAddress, + assets: Core.Assets.fromLovelace(1_000_000n) + }) + .attachMetadata({ + label: 1n, + metadata: "Test" + }) + + // Get programs snapshot + const programs = builder.getPrograms() + + // Should have 2 programs (payToAddress + attachMetadata) + expect(programs.length).toBe(2) + expect(Array.isArray(programs)).toBe(true) + + // Add another operation + builder.payToAddress({ + address: myAddress, + assets: Core.Assets.fromLovelace(2_000_000n) + }) + + // Get programs again - should have 3 now + const programs2 = builder.getPrograms() + expect(programs2.length).toBe(3) + + // Original snapshot should still be 2 (immutable) + expect(programs.length).toBe(2) + }) + + it("should compose builders created from different clients", { timeout: 60_000 }, async () => { + const client1 = createTestClient(0) + const client2 = createTestClient(1) + + const address1 = await client1.address() + const address2 = await client2.address() + + // Create builders from different clients + const builder1 = client1.newTx().payToAddress({ + address: address1, + assets: Core.Assets.fromLovelace(5_000_000n) + }) + + const builder2 = client2.newTx().attachMetadata({ + label: 42n, + metadata: "Cross-client composition" + }) + + // Compose them together using client1 + const signBuilder = await client1 + .newTx() + .compose(builder1) + .compose(builder2) + .payToAddress({ + address: address2, + assets: Core.Assets.fromLovelace(3_000_000n) + }) + .build() + + const tx = await signBuilder.toTransaction() + + // Verify combined operations + expect(tx.body.outputs.length).toBeGreaterThanOrEqual(2) + expect(tx.auxiliaryData).toBeDefined() + + // Submit and confirm + const submitBuilder = await signBuilder.sign() + const txHash = await submitBuilder.submit() + expect(txHash.length).toBe(64) + + const confirmed = await client1.awaitTx(txHash, 1000) + expect(confirmed).toBe(true) + }) +}) diff --git a/packages/evolution-devnet/test/TxBuilder.Scripts.test.ts b/packages/evolution-devnet/test/TxBuilder.Scripts.test.ts index 9d2db80a..24d25364 100644 --- a/packages/evolution-devnet/test/TxBuilder.Scripts.test.ts +++ b/packages/evolution-devnet/test/TxBuilder.Scripts.test.ts @@ -1,4 +1,5 @@ import { afterAll, beforeAll, describe, expect, it } from "@effect/vitest" +import { createAikenEvaluator } from "@evolution-sdk/aiken-uplc" import { Core } from "@evolution-sdk/evolution" import * as CoreAddress from "@evolution-sdk/evolution/core/Address" import * as Bytes from "@evolution-sdk/evolution/core/Bytes" @@ -199,6 +200,77 @@ describe("TxBuilder Script Handling", () => { expect(redeemer.exUnits.mem).toBeGreaterThan(0n) // mem > 0 expect(redeemer.exUnits.steps).toBeGreaterThan(0n) // steps > 0 }) + + it("should build transaction with Aiken WASM evaluator", async () => { + const alwaysSucceedsScript = makePlutusV2Script(ALWAYS_SUCCEED_SCRIPT_CBOR) + const scriptAddress = scriptToAddress(ALWAYS_SUCCEED_SCRIPT_CBOR) + + // Create script UTxO with inline datum + const ownerPubKeyHash = "00000000000000000000000000000000000000000000000000000000" + const datum = Data.toCBORHex(Data.constr(0n, [Data.bytearray(ownerPubKeyHash)])) + + const scriptUtxo = createCoreTestUtxo({ + transactionId: "a".repeat(64), + index: 0, + address: scriptAddress, + lovelace: 5_000_000n, + datumOption: { type: "inlineDatum", inline: datum } + }) + + // Create funding UTxO + const fundingUtxo = createCoreTestUtxo({ + transactionId: "b".repeat(64), + index: 0, + address: CHANGE_ADDRESS, + lovelace: 10_000_000n + }) + + // Create redeemer + const redeemerData = Data.constr(0n, [Data.bytearray("48656c6c6f2c20576f726c6421")]) + + const builder = makeTxBuilder(baseConfig) + .collectFrom({ + inputs: [scriptUtxo], + redeemer: redeemerData + }) + .attachScript({ script: alwaysSucceedsScript }) + .payToAddress({ + address: CoreAddress.fromBech32(RECEIVER_ADDRESS), + assets: CoreAssets.fromLovelace(2_000_000n) + }) + + // Build with Aiken evaluator and debug enabled + const signBuilder = await builder.build({ + changeAddress: CoreAddress.fromBech32(CHANGE_ADDRESS), + availableUtxos: [fundingUtxo], + protocolParameters: PROTOCOL_PARAMS, + evaluator: createAikenEvaluator, + debug: true // Enable debug logging + }) + + const tx = await signBuilder.toTransaction() + + // Verify transaction structure + expect(tx.body.inputs.length).toBe(1) + expect(tx.body.outputs.length).toBeGreaterThanOrEqual(2) // Payment + change + + // Verify script witnesses + expect(tx.witnessSet.plutusV2Scripts).toBeDefined() + expect(tx.witnessSet.plutusV2Scripts!.length).toBe(1) + + // Verify redeemers with evaluated exUnits + expect(tx.witnessSet.redeemers).toBeDefined() + expect(tx.witnessSet.redeemers!.length).toBe(1) + + const redeemer = tx.witnessSet.redeemers![0] + expect(redeemer.tag).toBe("spend") + expect(redeemer.exUnits.mem).toBeGreaterThan(0n) // mem > 0 + expect(redeemer.exUnits.steps).toBeGreaterThan(0n) // steps > 0 + + // eslint-disable-next-line no-console + console.log(`✓ Aiken evaluator: mem=${redeemer.exUnits.mem}, steps=${redeemer.exUnits.steps}`) + }) + it("should handle collateral inputs with multiassets and return excess to user as collateral return", async () => { const alwaysSucceedsScript = makePlutusV2Script(ALWAYS_SUCCEED_SCRIPT_CBOR) const scriptAddress = scriptToAddress(ALWAYS_SUCCEED_SCRIPT_CBOR) diff --git a/packages/evolution-devnet/test/TxBuilder.Validity.test.ts b/packages/evolution-devnet/test/TxBuilder.Validity.test.ts index 7da30907..4fe1b0b3 100644 --- a/packages/evolution-devnet/test/TxBuilder.Validity.test.ts +++ b/packages/evolution-devnet/test/TxBuilder.Validity.test.ts @@ -90,7 +90,7 @@ describe("TxBuilder Validity Interval", () => { } }, 60_000) - it("should build transaction with TTL and convert to slot correctly", { timeout: 60_000 }, async () => { + it("should build and submit transaction with TTL", { timeout: 60_000 }, async () => { const client = createTestClient(0) const myAddress = await client.address() @@ -113,9 +113,17 @@ describe("TxBuilder Validity Interval", () => { expect(typeof tx.body.ttl).toBe("bigint") expect(tx.body.ttl! > 0n).toBe(true) expect(tx.body.validityIntervalStart).toBeUndefined() + + // Submit and confirm + const submitBuilder = await signBuilder.sign() + const txHash = await submitBuilder.submit() + expect(txHash.length).toBe(64) + + const confirmed = await client.awaitTx(txHash, 1000) + expect(confirmed).toBe(true) }) - it("should build transaction with both validity bounds and convert to slots", { timeout: 60_000 }, async () => { + it("should build and submit transaction with both validity bounds", { timeout: 60_000 }, async () => { const client = createTestClient(0) const myAddress = await client.address() @@ -130,7 +138,7 @@ describe("TxBuilder Validity Interval", () => { address: myAddress, assets: Core.Assets.fromLovelace(5_000_000n) }) - .build({ availableUtxos: [...genesisUtxos] }) + .build() const tx = await signBuilder.toTransaction() @@ -145,6 +153,14 @@ describe("TxBuilder Validity Interval", () => { // TTL should be after validity start expect(tx.body.ttl! > tx.body.validityIntervalStart!).toBe(true) + + // Submit and confirm + const submitBuilder = await signBuilder.sign() + const txHash = await submitBuilder.submit() + expect(txHash.length).toBe(64) + + const confirmed = await client.awaitTx(txHash, 1000) + expect(confirmed).toBe(true) }) it("should reject expired transaction", { timeout: 60_000 }, async () => { diff --git a/packages/evolution/docs/modules/core/Metadata.ts.md b/packages/evolution/docs/modules/core/Metadata.ts.md index 86dada09..74b50743 100644 --- a/packages/evolution/docs/modules/core/Metadata.ts.md +++ b/packages/evolution/docs/modules/core/Metadata.ts.md @@ -497,8 +497,8 @@ Added in v2.0.0 ## MetadataLabel -Schema for transaction metadatum label (uint - unbounded positive integer). -Uses Numeric.NonNegativeInteger for consistency with other numeric types. +Schema for transaction metadatum label (uint64 per Cardano CDDL spec). +Labels must be in range 0 to 2^64-1. **Signature** diff --git a/packages/evolution/docs/modules/sdk/builders/TransactionBuilder.ts.md b/packages/evolution/docs/modules/sdk/builders/TransactionBuilder.ts.md index 100c3ef5..210f675b 100644 --- a/packages/evolution/docs/modules/sdk/builders/TransactionBuilder.ts.md +++ b/packages/evolution/docs/modules/sdk/builders/TransactionBuilder.ts.md @@ -55,8 +55,6 @@ double-spending. UTxOs can come from any source (wallet, DeFi protocols, other p - [formatFailures (method)](#formatfailures-method) - [ScriptFailure (interface)](#scriptfailure-interface) - [TransactionBuilderError (class)](#transactionbuildererror-class) -- [evaluators](#evaluators) - - [createUPLCEvaluator](#createuplcevaluator) - [model](#model) - [ChainResult (interface)](#chainresult-interface) - [EvaluationContext (interface)](#evaluationcontext-interface) @@ -67,7 +65,6 @@ double-spending. UTxOs can come from any source (wallet, DeFi protocols, other p - [TxBuilderState (interface)](#txbuilderstate-interface) - [types](#types) - [ProgramStep (type alias)](#programstep-type-alias) - - [UPLCEvalFunction (type alias)](#uplcevalfunction-type-alias) - [utils](#utils) - [BuildOptions (interface)](#buildoptions-interface) - [PhaseContextTag (class)](#phasecontexttag-class) @@ -633,7 +630,7 @@ export interface TransactionBuilderBase { /** * Attach metadata to the transaction. * - * Metadata is stored in the auxiliary data section and identified by labels (0-255) + * Metadata is stored in the auxiliary data section and identified by numeric labels * following the CIP-10 standard. Common use cases include: * - Transaction messages/comments (label 674, CIP-20) * - NFT metadata (label 721, CIP-25) @@ -648,28 +645,19 @@ export interface TransactionBuilderBase { * * @example * ```typescript - * import * as TransactionMetadatum from "@evolution-sdk/core/TransactionMetadatum" + * import { fromEntries } from "@evolution-sdk/evolution/core/TransactionMetadatum" * * // Attach a simple message (CIP-20) * const tx = await builder * .payToAddress({ address, assets: { lovelace: 2_000_000n } }) - * .attachMetadata({ - * label: 674n, - * metadata: TransactionMetadatum.makeTransactionMetadatumMap( - * new Map([[0n, TransactionMetadatum.makeTransactionMetadatumText("Hello, Cardano!")]]) - * ) - * }) + * .attachMetadata({ label: 674n, metadata: "Hello, Cardano!" }) * .build() * * // Attach NFT metadata (CIP-25) - * const nftMetadata = TransactionMetadatum.makeTransactionMetadatumMap( - * new Map([ - * [TransactionMetadatum.makeTransactionMetadatumText("name"), - * TransactionMetadatum.makeTransactionMetadatumText("My NFT #42")], - * [TransactionMetadatum.makeTransactionMetadatumText("image"), - * TransactionMetadatum.makeTransactionMetadatumText("ipfs://Qm...")], - * ]) - * ) + * const nftMetadata = fromEntries([ + * ["name", "My NFT #42"], + * ["image", "ipfs://Qm..."] + * ]) * const tx = await builder * .mintAssets({ assets: { [policyId + assetName]: 1n } }) * .attachMetadata({ label: 721n, metadata: nftMetadata }) @@ -681,6 +669,65 @@ export interface TransactionBuilderBase { */ readonly attachMetadata: (params: AttachMetadataParams) => this + // ============================================================================ + // Composition Methods + // ============================================================================ + + /** + * Compose this builder with another builder's accumulated operations. + * + * Merges all queued operations from another transaction builder into this one. + * The other builder's programs are captured at compose time and will be executed + * when build() is called on this builder. + * + * This enables modular transaction building where common patterns can be + * encapsulated in reusable builder fragments. + * + * **Important**: Composition is one-way - changes to the other builder after + * compose() is called will not affect this builder. + * + * @example + * ```typescript + * // Create reusable builder for common operations + * const mintBuilder = builder + * .mintAssets({ policyId, assets: { tokenName: 1n }, redeemer }) + * .attachScript({ script: mintingPolicy }) + * + * // Compose into a transaction that also pays to an address + * const tx = await builder + * .payToAddress({ address, assets: { lovelace: 5_000_000n } }) + * .compose(mintBuilder) + * .build() + * + * // Compose multiple builders + * const fullTx = await builder + * .compose(mintBuilder) + * .compose(metadataBuilder) + * .compose(certBuilder) + * .build() + * ``` + * + * @param other - Another transaction builder whose operations will be merged + * @returns The same builder for method chaining + * + * @since 2.0.0 + * @category composition-methods + */ + readonly compose: (other: TransactionBuilder) => this + + /** + * Get a snapshot of the accumulated programs. + * + * Returns a read-only copy of all queued operations that have been added + * to this builder. Useful for inspection, debugging, or advanced composition patterns. + * + * @returns Read-only array of accumulated program steps + * + * @since 2.0.0 + * @category composition-methods + */ + readonly getPrograms: () => ReadonlyArray + // ============================================================================ // Transaction Chaining Methods // ============================================================================ @@ -1079,23 +1126,6 @@ export declare class TransactionBuilderError Added in v2.0.0 -# evaluators - -## createUPLCEvaluator - -Creates an evaluator from a standard UPLC evaluation function. - -**NOTE: NOT YET IMPLEMENTED** - This function currently returns an evaluator -that produces dummy data. Reserved for future UPLC script evaluation support. - -**Signature** - -```ts -export declare const createUPLCEvaluator: (_evalFunction: UPLCEvalFunction) => Evaluator -``` - -Added in v2.0.0 - # model ## ChainResult (interface) @@ -1168,7 +1198,7 @@ export interface Evaluator { * @category methods */ evaluate: ( - tx: string, + tx: Transaction.Transaction, additionalUtxos: ReadonlyArray | undefined, context: EvaluationContext ) => Effect.Effect, EvaluationError> @@ -1301,30 +1331,6 @@ export type ProgramStep = Effect.Effect, - utxos_bytes_y: Array, - cost_mdls_bytes: Uint8Array, - initial_budget_n: bigint, - initial_budget_d: bigint, - slot_config_x: bigint, - slot_config_y: bigint, - slot_config_z: number -) => Array -``` - -Added in v2.0.0 - # utils ## BuildOptions (interface) diff --git a/packages/evolution/docs/modules/sdk/builders/TxBuilderImpl.ts.md b/packages/evolution/docs/modules/sdk/builders/TxBuilderImpl.ts.md index 0ce512cc..994ea57d 100644 --- a/packages/evolution/docs/modules/sdk/builders/TxBuilderImpl.ts.md +++ b/packages/evolution/docs/modules/sdk/builders/TxBuilderImpl.ts.md @@ -78,7 +78,7 @@ Uses Core UTxO types directly. ```ts export declare const buildTransactionInputs: ( utxos: ReadonlyArray -) => Effect.Effect, TransactionBuilderError> +) => Effect.Effect, never> ``` Added in v2.0.0 @@ -187,7 +187,7 @@ export declare const calculateFeeIteratively: ( } >, protocolParams: { minFeeCoefficient: bigint; minFeeConstant: bigint; priceMem?: number; priceStep?: number } -) => Effect.Effect +) => Effect.Effect ``` Added in v2.0.0 @@ -350,7 +350,7 @@ export declare const makeTxOutput: (params: { assets: CoreAssets.Assets datum?: DatumOption.DatumOption scriptRef?: CoreScript.Script -}) => Effect.Effect +}) => Effect.Effect ``` Added in v2.0.0 @@ -368,7 +368,7 @@ Use case: Draining wallet by merging leftover into an existing payment output. export declare const mergeAssetsIntoOutput: ( output: TxOut.TransactionOutput, additionalAssets: CoreAssets.Assets -) => Effect.Effect +) => Effect.Effect ``` Added in v2.0.0 @@ -386,7 +386,7 @@ Use case: Draining wallet by merging leftover into an existing payment output. export declare const mergeAssetsIntoUTxO: ( utxo: CoreUTxO.UTxO, additionalAssets: CoreAssets.Assets -) => Effect.Effect +) => Effect.Effect ``` Added in v2.0.0 diff --git a/packages/evolution/docs/modules/sdk/builders/operations/Attach.ts.md b/packages/evolution/docs/modules/sdk/builders/operations/Attach.ts.md index f8280ca1..ab7fbd1c 100644 --- a/packages/evolution/docs/modules/sdk/builders/operations/Attach.ts.md +++ b/packages/evolution/docs/modules/sdk/builders/operations/Attach.ts.md @@ -28,9 +28,7 @@ Scripts must be attached before being referenced by transaction inputs or mintin **Signature** ```ts -export declare const attachScriptToState: ( - script: ScriptCore.Script -) => Effect.Effect +export declare const attachScriptToState: (script: ScriptCore.Script) => Effect.Effect ``` Added in v2.0.0 diff --git a/packages/evolution/docs/modules/sdk/builders/operations/AttachMetadata.ts.md b/packages/evolution/docs/modules/sdk/builders/operations/AttachMetadata.ts.md index bd1fb530..fbf9f7f2 100644 --- a/packages/evolution/docs/modules/sdk/builders/operations/AttachMetadata.ts.md +++ b/packages/evolution/docs/modules/sdk/builders/operations/AttachMetadata.ts.md @@ -42,7 +42,7 @@ Implementation: ```ts export declare const createAttachMetadataProgram: ( params: AttachMetadataParams -) => Effect.Effect +) => Effect.Effect ``` Added in v2.0.0 diff --git a/packages/evolution/docs/modules/sdk/builders/operations/Pay.ts.md b/packages/evolution/docs/modules/sdk/builders/operations/Pay.ts.md index 4715a81b..e61b1cd6 100644 --- a/packages/evolution/docs/modules/sdk/builders/operations/Pay.ts.md +++ b/packages/evolution/docs/modules/sdk/builders/operations/Pay.ts.md @@ -35,9 +35,7 @@ Implementation: **Signature** ```ts -export declare const createPayToAddressProgram: ( - params: PayToAddressParams -) => Effect.Effect +export declare const createPayToAddressProgram: (params: PayToAddressParams) => Effect.Effect ``` Added in v2.0.0 diff --git a/packages/evolution/docs/modules/sdk/builders/phases/FeeCalculation.ts.md b/packages/evolution/docs/modules/sdk/builders/phases/FeeCalculation.ts.md index 82cfd044..795fc0f5 100644 --- a/packages/evolution/docs/modules/sdk/builders/phases/FeeCalculation.ts.md +++ b/packages/evolution/docs/modules/sdk/builders/phases/FeeCalculation.ts.md @@ -68,6 +68,6 @@ goto balance export declare const executeFeeCalculation: () => Effect.Effect< PhaseResult, TransactionBuilderError, - PhaseContextTag | TxContext | ProtocolParametersTag + PhaseContextTag | TxContext | ProtocolParametersTag | BuildOptionsTag > ``` diff --git a/packages/evolution/src/core/Redeemer.ts b/packages/evolution/src/core/Redeemer.ts index d9d2fab1..7ad8212e 100644 --- a/packages/evolution/src/core/Redeemer.ts +++ b/packages/evolution/src/core/Redeemer.ts @@ -7,12 +7,12 @@ import * as Numeric from "./Numeric.js" /** * Redeemer tag enum for different script execution contexts. * - * CDDL: redeemer_tag = 0 ; spend | 1 ; mint | 2 ; cert | 3 ; reward + * CDDL: redeemer_tag = 0 ; spend | 1 ; mint | 2 ; cert | 3 ; reward | 4 ; vote | 5 ; propose * * @since 2.0.0 * @category model */ -export const RedeemerTag = Schema.Literal("spend", "mint", "cert", "reward").annotations({ +export const RedeemerTag = Schema.Literal("spend", "mint", "cert", "reward", "vote", "propose").annotations({ identifier: "Redeemer.Tag", title: "Redeemer Tag", description: "Tag indicating the context where the redeemer is used" @@ -168,6 +168,10 @@ export const tagToInteger = (tag: RedeemerTag): bigint => { return 2n case "reward": return 3n + case "vote": + return 4n + case "propose": + return 5n } } @@ -187,9 +191,13 @@ export const integerToTag = (value: bigint): RedeemerTag => { return "cert" case 3n: return "reward" + case 4n: + return "vote" + case 5n: + return "propose" default: throw new Error( - `Invalid redeemer tag: ${value}. Must be 0 (spend), 1 (mint), 2 (cert), or 3 (reward)` + `Invalid redeemer tag: ${value}. Must be 0 (spend), 1 (mint), 2 (cert), 3 (reward), 4 (vote), or 5 (propose)` ) } } diff --git a/packages/evolution/src/sdk/builders/SignBuilderImpl.ts b/packages/evolution/src/sdk/builders/SignBuilderImpl.ts index f38b104f..730e3ef3 100644 --- a/packages/evolution/src/sdk/builders/SignBuilderImpl.ts +++ b/packages/evolution/src/sdk/builders/SignBuilderImpl.ts @@ -108,7 +108,7 @@ export const makeSignBuilder = (params: { const walletWitnessSet = yield* wallet.Effect.signTx(transaction, { utxos, referenceUtxos }).pipe( Effect.mapError( (walletError) => - new TransactionBuilderError({ message: "Failed to sign transaction", cause: walletError }) + new TransactionBuilderError({ message: `Failed to sign transaction: ${walletError.message}`, cause: walletError }) ) ) @@ -257,7 +257,7 @@ export const makeSignBuilder = (params: { const witnessSet = yield* wallet.Effect.signTx(transaction, { utxos, referenceUtxos }).pipe( Effect.mapError( (walletError) => - new TransactionBuilderError({ message: "Failed to create partial signature", cause: walletError }) + new TransactionBuilderError({ message: `Failed to create partial signature: ${walletError.message}`, cause: walletError }) ) ) diff --git a/packages/evolution/src/sdk/builders/SubmitBuilderImpl.ts b/packages/evolution/src/sdk/builders/SubmitBuilderImpl.ts index 2380fc6a..d1b07bb1 100644 --- a/packages/evolution/src/sdk/builders/SubmitBuilderImpl.ts +++ b/packages/evolution/src/sdk/builders/SubmitBuilderImpl.ts @@ -42,7 +42,10 @@ export const makeSubmitBuilder = ( const txHash = yield* provider.Effect.submitTx(txCborHex).pipe( Effect.mapError( (providerError) => - new TransactionBuilderError({ message: "Failed to submit transaction", cause: providerError }) + new TransactionBuilderError({ + message: `Failed to submit transaction: ${providerError.message}`, + cause: providerError + }) ) ) diff --git a/packages/evolution/src/sdk/builders/TransactionBuilder.ts b/packages/evolution/src/sdk/builders/TransactionBuilder.ts index 6456fd1b..882b6056 100644 --- a/packages/evolution/src/sdk/builders/TransactionBuilder.ts +++ b/packages/evolution/src/sdk/builders/TransactionBuilder.ts @@ -54,10 +54,36 @@ import { attachScriptToState } from "./operations/Attach.js" import { createAttachMetadataProgram } from "./operations/AttachMetadata.js" import { createCollectFromProgram } from "./operations/Collect.js" import { createMintProgram } from "./operations/Mint.js" -import type { AddSignerParams, AttachMetadataParams, AuthCommitteeHotParams, CollectFromParams, DelegateToParams, DeregisterDRepParams, DeregisterStakeParams, MintTokensParams, PayToAddressParams, ReadFromParams, RegisterAndDelegateToParams, RegisterDRepParams, RegisterPoolParams, RegisterStakeParams, ResignCommitteeColdParams, RetirePoolParams, UpdateDRepParams, ValidityParams, WithdrawParams } from "./operations/Operations.js" +import type { + AddSignerParams, + AttachMetadataParams, + AuthCommitteeHotParams, + CollectFromParams, + DelegateToParams, + DeregisterDRepParams, + DeregisterStakeParams, + MintTokensParams, + PayToAddressParams, + ReadFromParams, + RegisterAndDelegateToParams, + RegisterDRepParams, + RegisterPoolParams, + RegisterStakeParams, + ResignCommitteeColdParams, + RetirePoolParams, + UpdateDRepParams, + ValidityParams, + WithdrawParams +} from "./operations/Operations.js" import { createPayToAddressProgram } from "./operations/Pay.js" import { createReadFromProgram } from "./operations/ReadFrom.js" -import { createDelegateToProgram, createDeregisterStakeProgram, createRegisterAndDelegateToProgram, createRegisterStakeProgram, createWithdrawProgram } from "./operations/Stake.js" +import { + createDelegateToProgram, + createDeregisterStakeProgram, + createRegisterAndDelegateToProgram, + createRegisterStakeProgram, + createWithdrawProgram +} from "./operations/Stake.js" import { createSetValidityProgram } from "./operations/Validity.js" import { executeBalance } from "./phases/Balance.js" import { executeChangeCreation } from "./phases/ChangeCreation.js" @@ -92,7 +118,15 @@ export class TransactionBuilderError extends Data.TaggedError("TransactionBuilde /** * Build phases */ -type Phase = "selection" | "changeCreation" | "feeCalculation" | "balance" | "evaluation" | "collateral" | "fallback" | "complete" +type Phase = + | "selection" + | "changeCreation" + | "feeCalculation" + | "balance" + | "evaluation" + | "collateral" + | "fallback" + | "complete" /** * BuildContext - state machine context @@ -202,10 +236,7 @@ const resolveAvailableUtxos = ( } if (config.wallet && config.provider) { - return Effect.flatMap( - config.wallet.Effect.address(), - (addr) => config.provider!.Effect.getUtxos(addr) - ) + return Effect.flatMap(config.wallet.Effect.address(), (addr) => config.provider!.Effect.getUtxos(addr)) } return Effect.fail( @@ -217,6 +248,97 @@ const resolveAvailableUtxos = ( ) } +/** + * Ogmios error response structure for script failures. + */ +interface OgmiosValidatorError { + validator: { index: number; purpose: string } + error: { + code: number + message: string + data: { + validationError: string + traces: Array + } + } +} + +/** + * Parse Ogmios/provider error response into raw ScriptFailure array. + * Returns failures without labels - enrichment happens in Evaluation phase. + */ +const parseProviderError = (error: unknown): Array => { + const failures: Array = [] + + // Navigate through error chain to find the response body + const findErrorData = (e: unknown): Array | undefined => { + if (!e || typeof e !== "object") return undefined + + const obj = e as Record + + // Direct data property (from ProviderError cause chain) + if (obj.cause && typeof obj.cause === "object") { + const cause = obj.cause as Record + + // ResponseError with response.body + if (cause.response && typeof cause.response === "object") { + const resp = cause.response as Record + if (resp.body && typeof resp.body === "object") { + const body = resp.body as Record + if (body.error && typeof body.error === "object") { + const err = body.error as Record + if (Array.isArray(err.data)) { + return err.data as Array + } + } + } + } + + // Try description field which contains the JSON string + if (typeof cause.description === "string") { + try { + const match = cause.description.match(/\{.*\}/s) + if (match) { + const parsed = JSON.parse(match[0]) + if (parsed.error?.data && Array.isArray(parsed.error.data)) { + return parsed.error.data as Array + } + } + } catch { + // JSON parse failed, continue looking + } + } + + // Recurse into cause + return findErrorData(cause) + } + + return undefined + } + + const errorData = findErrorData(error) + + if (!errorData) { + return failures + } + + // Process each validator error (raw, without labels) + for (const validatorError of errorData) { + const { error: err, validator } = validatorError + const { index, purpose } = validator + const { traces, validationError } = err.data + + failures.push({ + purpose, + index, + validationError, + traces: traces ?? [] + }) + } + + return failures +} + /** * Resolve evaluator from options, provider, or return undefined. * Priority: BuildOptions.evaluator > provider.evaluateTx (wrapped) > undefined @@ -233,16 +355,25 @@ const resolveEvaluator = (config: TxBuilderConfig, options?: BuildOptions): Eval // Priority 2: Wrap provider's evaluateTx as an Evaluator if (config.provider) { return { - evaluate: (tx: string, additionalUtxos: ReadonlyArray | undefined, _context: EvaluationContext) => - config.provider!.Effect.evaluateTx(tx, additionalUtxos as Array | undefined).pipe( - Effect.mapError( - (providerError) => - new EvaluationError({ - message: "Provider evaluation failed", - cause: providerError - }) - ) + evaluate: ( + tx: Transaction.Transaction, + additionalUtxos: ReadonlyArray | undefined, + _context: EvaluationContext + ) => { + // Serialize Transaction to CBOR hex for provider + const txHex = Transaction.toCBORHex(tx) + return config.provider!.Effect.evaluateTx(txHex, additionalUtxos as Array | undefined).pipe( + Effect.mapError((providerError) => { + // Parse provider error into structured failures + const failures = parseProviderError(providerError) + return new EvaluationError({ + message: `Provider evaluation failed: ${providerError.message}`, + cause: providerError, + failures + }) + }) ) + } } } @@ -363,7 +494,7 @@ const assembleAndValidateTransaction = Effect.gen(function* () { // SAFETY CHECK: Validate transaction size against protocol limit // Include collateral UTxOs in witness estimation - they require VKey witnesses too! - const allUtxosForWitnesses = finalState.collateral + const allUtxosForWitnesses = finalState.collateral ? [...selectedUtxos, ...finalState.collateral.inputs] : selectedUtxos const fakeWitnessSet = yield* buildFakeWitnessSet(allUtxosForWitnesses) @@ -504,7 +635,7 @@ const buildPartialEffectCore = ( Effect.mapError( (error) => new TransactionBuilderError({ - message: "Partial build failed", + message: `Partial build failed: ${error.message}`, cause: error }) ) @@ -531,20 +662,13 @@ export interface ChainResult { readonly txHash: string } -// ============================================================================ -// Evaluator Interface - Generic abstraction for script evaluation -// ============================================================================ -// NOTE: These interfaces are reserved for future UPLC script evaluation support. -// The createUPLCEvaluator function currently returns dummy data and is not yet implemented. - /** * Data required by script evaluators: cost models, execution limits, and slot configuration. * - * **NOTE: NOT YET IMPLEMENTED** - Reserved for future UPLC script evaluation support. + * Used by custom evaluators for local UPLC script evaluation. * * @since 2.0.0 * @category model - * @experimental */ export interface EvaluationContext { /** Cost models for script evaluation */ @@ -564,12 +688,10 @@ export interface EvaluationContext { /** * Interface for evaluating transaction scripts and computing execution units. * - * **NOTE: NOT YET IMPLEMENTED** - Reserved for future custom script evaluation support. - * When implemented, this will enable custom evaluation strategies including local UPLC execution. + * Implement this interface to provide custom script evaluation strategies, such as local UPLC execution. * * @since 2.0.0 * @category model - * @experimental */ export interface Evaluator { /** @@ -579,7 +701,7 @@ export interface Evaluator { * @category methods */ evaluate: ( - tx: string, + tx: Transaction.Transaction, additionalUtxos: ReadonlyArray | undefined, context: EvaluationContext ) => Effect.Effect, EvaluationError> @@ -630,90 +752,7 @@ export class EvaluationError extends Data.TaggedError("EvaluationError")<{ readonly message?: string /** Parsed script failures with labels */ readonly failures?: ReadonlyArray -}> { - /** - * Format failures into a human-readable string. - */ - formatFailures(): string { - if (!this.failures || this.failures.length === 0) { - return this.message ?? "Script evaluation failed" - } - - const lines = ["Script evaluation failed:"] - for (const f of this.failures) { - const labelPart = f.label ? ` [${f.label}]` : "" - const refPart = f.utxoRef ? ` UTxO: ${f.utxoRef}` : f.credential ? ` Credential: ${f.credential}` : f.policyId ? ` Policy: ${f.policyId}` : "" - lines.push(` ${f.purpose}:${f.index}${labelPart}${refPart}`) - lines.push(` Error: ${f.validationError}`) - if (f.traces.length > 0) { - lines.push(` Traces: ${f.traces.join(", ")}`) - } - } - return lines.join("\n") - } -} - -// ============================================================================ -// Factory Functions -// ============================================================================ - -/** - * Standard UPLC evaluation function signature (matches UPLC.eval_phase_two_raw). - * - * **NOTE: NOT YET IMPLEMENTED** - Reserved for future UPLC evaluation support. - * - * @since 2.0.0 - * @category types - * @experimental - */ -export type UPLCEvalFunction = ( - tx_bytes: Uint8Array, - utxos_bytes_x: Array, - utxos_bytes_y: Array, - cost_mdls_bytes: Uint8Array, - initial_budget_n: bigint, - initial_budget_d: bigint, - slot_config_x: bigint, - slot_config_y: bigint, - slot_config_z: number -) => Array - -/** - * Creates an evaluator from a standard UPLC evaluation function. - * - * **NOTE: NOT YET IMPLEMENTED** - This function currently returns an evaluator - * that produces dummy data. Reserved for future UPLC script evaluation support. - * - * @since 2.0.0 - * @category evaluators - * @experimental - */ -export const createUPLCEvaluator = (_evalFunction: UPLCEvalFunction): Evaluator => ({ - evaluate: (_tx: string, _additionalUtxos: ReadonlyArray | undefined, _context: EvaluationContext) => - Effect.gen(function* () { - // Implementation: Call UPLC evaluation with provided parameters - // _evalFunction( - // fromHex(_tx), - // utxosToInputBytes(_additionalUtxos), - // utxosToOutputBytes(_additionalUtxos), - // _context.costModels, - // _context.maxTxExSteps, - // _context.maxTxExMem, - // _context.slotConfig.zeroTime, - // _context.slotConfig.zeroSlot, - // _context.slotConfig.slotLength - // ) - - // Return dummy EvalRedeemer for now - const dummyEvalRedeemer: EvalRedeemer = { - ex_units: { mem: 1000000, steps: 5000000 }, - redeemer_index: 0, - redeemer_tag: "spend" - } - - return [dummyEvalRedeemer] as const - }) -}) +}> {} // ============================================================================ // Provider Integration @@ -1407,7 +1446,10 @@ export class TxBuilderConfigTag extends Context.Tag("TxBuilderConfig")>() {} +export class AvailableUtxosTag extends Context.Tag("AvailableUtxos")< + AvailableUtxosTag, + ReadonlyArray +>() {} /** * Context tag providing BuildOptions for the current build. @@ -1581,9 +1623,9 @@ export interface TransactionBuilderBase { * ```typescript * // Mint tokens from a native script policy * const tx = await builder - * .mintAssets({ - * assets: { - * "": 1000n + * .mintAssets({ + * assets: { + * "": 1000n * } * }) * .build() @@ -1591,9 +1633,9 @@ export interface TransactionBuilderBase { * // Mint from Plutus script policy with redeemer * const tx = await builder * .attachScript(mintingScript) - * .mintAssets({ - * assets: { - * "": 1000n + * .mintAssets({ + * assets: { + * "": 1000n * }, * redeemer: myRedeemer * }) @@ -1947,6 +1989,65 @@ export interface TransactionBuilderBase { */ readonly attachMetadata: (params: AttachMetadataParams) => this + // ============================================================================ + // Composition Methods + // ============================================================================ + + /** + * Compose this builder with another builder's accumulated operations. + * + * Merges all queued operations from another transaction builder into this one. + * The other builder's programs are captured at compose time and will be executed + * when build() is called on this builder. + * + * This enables modular transaction building where common patterns can be + * encapsulated in reusable builder fragments. + * + * **Important**: Composition is one-way - changes to the other builder after + * compose() is called will not affect this builder. + * + * @example + * ```typescript + * // Create reusable builder for common operations + * const mintBuilder = builder + * .mintAssets({ policyId, assets: { tokenName: 1n }, redeemer }) + * .attachScript({ script: mintingPolicy }) + * + * // Compose into a transaction that also pays to an address + * const tx = await builder + * .payToAddress({ address, assets: { lovelace: 5_000_000n } }) + * .compose(mintBuilder) + * .build() + * + * // Compose multiple builders + * const fullTx = await builder + * .compose(mintBuilder) + * .compose(metadataBuilder) + * .compose(certBuilder) + * .build() + * ``` + * + * @param other - Another transaction builder whose operations will be merged + * @returns The same builder for method chaining + * + * @since 2.0.0 + * @category composition-methods + */ + readonly compose: (other: TransactionBuilder) => this + + /** + * Get a snapshot of the accumulated programs. + * + * Returns a read-only copy of all queued operations that have been added + * to this builder. Useful for inspection, debugging, or advanced composition patterns. + * + * @returns Read-only array of accumulated program steps + * + * @since 2.0.0 + * @category composition-methods + */ + readonly getPrograms: () => ReadonlyArray + // ============================================================================ // Transaction Chaining Methods // ============================================================================ @@ -2253,10 +2354,15 @@ export function makeTxBuilder(config: TxBuilderConfig) { programs.push(createAttachMetadataProgram(params)) return txBuilder }, + compose: (other: TransactionBuilder) => { + const otherPrograms = other.getPrograms() + if (otherPrograms.length > 0) { + programs.push(...otherPrograms) + } + return txBuilder + }, - // ============================================================================ - // Hybrid completion methods - Execute with fresh state - // ============================================================================ + getPrograms: () => [...programs], buildEffect: (options?: BuildOptions) => { return makeBuild(config, programs, options) diff --git a/packages/evolution/src/sdk/builders/TxBuilderImpl.ts b/packages/evolution/src/sdk/builders/TxBuilderImpl.ts index 95315a0a..ffd8775f 100644 --- a/packages/evolution/src/sdk/builders/TxBuilderImpl.ts +++ b/packages/evolution/src/sdk/builders/TxBuilderImpl.ts @@ -237,7 +237,7 @@ export const makeDatumOption = (datum: Datum.Datum): Effect.Effect new TransactionBuilderError({ - message: `Failed to parse datum: ${JSON.stringify(datum)}`, + message: `Failed to parse datum: ${error.message}`, cause: error }) ) @@ -257,7 +257,7 @@ export const makeTxOutput = (params: { assets: CoreAssets.Assets datum?: DatumOption.DatumOption scriptRef?: CoreScript.Script -}): Effect.Effect => +}): Effect.Effect => Effect.gen(function* () { // Convert Script to ScriptRef for CBOR encoding if provided const scriptRefEncoded = params.scriptRef @@ -273,15 +273,7 @@ export const makeTxOutput = (params: { }) return output - }).pipe( - Effect.mapError( - (error) => - new TransactionBuilderError({ - message: `Failed to create TxOutput`, - cause: error - }) - ) - ) + }) /** * Convert parameters to core TransactionOutput. @@ -297,7 +289,7 @@ export const txOutputToTransactionOutput = (params: { assets: CoreAssets.Assets datum?: DatumOption.DatumOption scriptRef?: CoreScript.Script -}): Effect.Effect => +}): Effect.Effect => Effect.gen(function* () { // Convert Script to ScriptRef for CBOR encoding if provided const scriptRefEncoded = params.scriptRef @@ -313,15 +305,7 @@ export const txOutputToTransactionOutput = (params: { }) return output - }).pipe( - Effect.mapError( - (error) => - new TransactionBuilderError({ - message: `Failed to create transaction output`, - cause: error - }) - ) - ) + }) /** * Merge additional assets into an existing UTxO. @@ -335,7 +319,7 @@ export const txOutputToTransactionOutput = (params: { export const mergeAssetsIntoUTxO = ( utxo: CoreUTxO.UTxO, additionalAssets: CoreAssets.Assets -): Effect.Effect => +): Effect.Effect => Effect.gen(function* () { // Merge assets using Core Assets helper const mergedAssets = CoreAssets.merge(utxo.assets, additionalAssets) @@ -348,15 +332,7 @@ export const mergeAssetsIntoUTxO = ( datumOption: utxo.datumOption, scriptRef: utxo.scriptRef }) - }).pipe( - Effect.mapError( - (error) => - new TransactionBuilderError({ - message: "Failed to merge assets into UTxO", - cause: error - }) - ) - ) + }) /** * Merge additional assets into an existing TransactionOutput. @@ -370,7 +346,7 @@ export const mergeAssetsIntoUTxO = ( export const mergeAssetsIntoOutput = ( output: TxOut.TransactionOutput, additionalAssets: CoreAssets.Assets -): Effect.Effect => +): Effect.Effect => Effect.gen(function* () { // Merge assets using Core Assets helper const mergedAssets = CoreAssets.merge(output.assets, additionalAssets) @@ -383,15 +359,7 @@ export const mergeAssetsIntoOutput = ( scriptRef: output.scriptRef }) return newOutput - }).pipe( - Effect.mapError( - (error) => - new TransactionBuilderError({ - message: "Failed to merge assets into output", - cause: error - }) - ) - ) + }) // ============================================================================ // Transaction Assembly @@ -407,7 +375,7 @@ export const mergeAssetsIntoOutput = ( */ export const buildTransactionInputs = ( utxos: ReadonlyArray -): Effect.Effect, TransactionBuilderError> => +): Effect.Effect, never> => Effect.gen(function* () { // Convert each Core UTxO to TransactionInput const inputs: Array = [] @@ -440,15 +408,7 @@ export const buildTransactionInputs = ( }) return inputs - }).pipe( - Effect.mapError( - (error) => - new TransactionBuilderError({ - message: "Failed to build transaction inputs", - cause: error - }) - ) - ) + }) /** * Assemble a Transaction from inputs, outputs, and calculated fee. @@ -692,7 +652,7 @@ export const assembleTransaction = ( Effect.mapError( (providerError) => new TransactionBuilderError({ - message: "Failed to fetch full protocol parameters for scriptDataHash calculation", + message: `Failed to fetch full protocol parameters for scriptDataHash calculation: ${providerError.message}`, cause: providerError }) ) @@ -822,7 +782,7 @@ export const assembleTransaction = ( Effect.mapError( (error) => new TransactionBuilderError({ - message: "Failed to assemble transaction", + message: `Failed to assemble transaction: ${error.message}`, cause: error }) ) @@ -858,7 +818,7 @@ export const calculateTransactionSize = ( Effect.mapError( (error) => new TransactionBuilderError({ - message: "Failed to calculate transaction size", + message: `Failed to calculate transaction size: ${error.message}`, cause: error }) ) @@ -1170,7 +1130,7 @@ export const calculateFeeIteratively = ( priceMem?: number priceStep?: number } -): Effect.Effect => +): Effect.Effect => Effect.gen(function* () { // Get state to access mint field and collateral const stateRef = yield* TxContext @@ -1258,12 +1218,27 @@ export const calculateFeeIteratively = ( ] } + // Convert validity interval to slots for fee calculation + // Validity fields affect transaction size and must be included + const buildOptions = yield* BuildOptionsTag + const slotConfig = buildOptions.slotConfig! + let ttl: bigint | undefined + let validityIntervalStart: bigint | undefined + if (state.validity?.to !== undefined) { + ttl = Time.unixTimeToSlot(state.validity.to, slotConfig) + } + if (state.validity?.from !== undefined) { + validityIntervalStart = Time.unixTimeToSlot(state.validity.from, slotConfig) + } + while (iterations < maxIterations) { // Build transaction with current fee estimate const body = new TransactionBody.TransactionBody({ inputs: inputs as Array, outputs: transactionOutputs, fee: currentFee, + ttl, // Include TTL for accurate size calculation + validityIntervalStart, // Include validity start for accurate size calculation mint, // Include mint field for accurate size calculation scriptDataHash: placeholderScriptDataHash, // Include scriptDataHash for accurate size auxiliaryDataHash: placeholderAuxiliaryDataHash, // Include auxiliaryDataHash for accurate size @@ -1341,7 +1316,7 @@ export const calculateFeeIteratively = ( Effect.mapError( (error) => new TransactionBuilderError({ - message: "Fee calculation failed to converge", + message: `Fee calculation failed to converge: ${error.message}`, cause: error }) ) diff --git a/packages/evolution/src/sdk/builders/operations/Attach.ts b/packages/evolution/src/sdk/builders/operations/Attach.ts index 2a3dc198..7bde6cad 100644 --- a/packages/evolution/src/sdk/builders/operations/Attach.ts +++ b/packages/evolution/src/sdk/builders/operations/Attach.ts @@ -2,7 +2,7 @@ import { Effect, Ref } from "effect" import type * as ScriptCore from "../../../core/Script.js" import * as ScriptHashCore from "../../../core/ScriptHash.js" -import { TransactionBuilderError, TxContext } from "../TransactionBuilder.js" +import { TxContext } from "../TransactionBuilder.js" /** * Attaches a script to the transaction by storing it in the builder state. @@ -33,12 +33,4 @@ export const attachScriptToState = (script: ScriptCore.Script) => ...state, scripts: updatedScripts }) - }).pipe( - Effect.mapError( - (error) => - new TransactionBuilderError({ - message: "Failed to attach script", - cause: error - }) - ) - ) + }) diff --git a/packages/evolution/src/sdk/builders/phases/Evaluation.ts b/packages/evolution/src/sdk/builders/phases/Evaluation.ts index 4939427f..20dd0a0f 100644 --- a/packages/evolution/src/sdk/builders/phases/Evaluation.ts +++ b/packages/evolution/src/sdk/builders/phases/Evaluation.ts @@ -12,8 +12,8 @@ import { Effect, Ref } from "effect" import * as Bytes from "../../../core/Bytes.js" import * as CostModel from "../../../core/CostModel.js" +import { INT64_MAX } from "../../../core/Numeric.js" import * as PolicyId from "../../../core/PolicyId.js" -import * as Transaction from "../../../core/Transaction.js" import * as CoreUTxO from "../../../core/UTxO.js" import type * as ProtocolParametersModule from "../../ProtocolParameters.js" import * as EvaluationStateManager from "../EvaluationStateManager.js" @@ -45,7 +45,10 @@ const costModelsToCBOR = ( Effect.gen(function* () { // Convert Record format to bigint arrays const plutusV1Costs = Object.values(protocolParams.costModels.PlutusV1).map((v) => BigInt(v)) - const plutusV2Costs = Object.values(protocolParams.costModels.PlutusV2).map((v) => BigInt(v)) + const plutusV2Costs = Object.values(protocolParams.costModels.PlutusV2) + .map((v) => BigInt(v)) + // Filter out devnet placeholder values (2^63) that overflow i64 in WASM CBOR decoder + .filter((v) => v <= INT64_MAX) const plutusV3Costs = Object.values(protocolParams.costModels.PlutusV3).map((v) => BigInt(v)) // Create CostModels instance @@ -67,94 +70,21 @@ const costModelsToCBOR = ( }) /** - * Ogmios error response structure for script failures. - */ -interface OgmiosValidatorError { - validator: { index: number; purpose: string } - error: { - code: number - message: string - data: { - validationError: string - traces: Array - } - } -} - -/** - * Parse Ogmios script evaluation error response and enrich with labels. + * Enrich raw script failures with labels and context from builder state. * - * Extracts structured failure information from the Ogmios JSON-RPC error response - * and maps redeemer indices back to user-provided labels from the builder state. + * Takes raw failures (from evaluator) and adds user-provided labels, + * UTxO references, credentials, and policy IDs based on index mappings. */ -const parseOgmiosError = ( - error: unknown, +const enrichFailuresWithLabels = ( + failures: ReadonlyArray, redeemers: Map, inputIndexMapping: Map, withdrawalIndexMapping: Map, mintIndexMapping: Map, certIndexMapping: Map ): Array => { - const failures: Array = [] - - // Navigate through error chain to find the response body - const findErrorData = (e: unknown): Array | undefined => { - if (!e || typeof e !== "object") return undefined - - // Check for ResponseError with parsed body - const obj = e as Record - - // Direct data property (from ProviderError cause chain) - if (obj.cause && typeof obj.cause === "object") { - const cause = obj.cause as Record - - // ResponseError with response.body - if (cause.response && typeof cause.response === "object") { - const resp = cause.response as Record - if (resp.body && typeof resp.body === "object") { - const body = resp.body as Record - if (body.error && typeof body.error === "object") { - const err = body.error as Record - if (Array.isArray(err.data)) { - return err.data as Array - } - } - } - } - - // Try description field which contains the JSON string - if (typeof cause.description === "string") { - try { - const match = cause.description.match(/\{.*\}/s) - if (match) { - const parsed = JSON.parse(match[0]) - if (parsed.error?.data && Array.isArray(parsed.error.data)) { - return parsed.error.data as Array - } - } - } catch { - // JSON parse failed, continue looking - } - } - - // Recurse into cause - return findErrorData(cause) - } - - return undefined - } - - const errorData = findErrorData(error) - - if (!errorData) { - return failures - } - - // Process each validator error - for (const validatorError of errorData) { - const { error: err, validator } = validatorError - const { index, purpose } = validator - const { traces, validationError } = err.data + return failures.map((failure) => { + const { index, purpose } = failure // Look up the redeemer key based on purpose and index let redeemerKey: string | undefined @@ -183,20 +113,15 @@ const parseOgmiosError = ( label = redeemer?.label } - failures.push({ - purpose, - index, + return { + ...failure, label, redeemerKey, utxoRef, credential, - policyId, - validationError, - traces: traces ?? [] - }) - } - - return failures + policyId + } + }) } /** @@ -382,7 +307,7 @@ export const executeEvaluation = (): Effect.Effect< Effect.mapError( (providerError) => new TransactionBuilderError({ - message: "Failed to fetch full protocol parameters for evaluation", + message: `Failed to fetch full protocol parameters for evaluation: ${providerError.message}`, cause: providerError }) ) @@ -519,20 +444,8 @@ export const executeEvaluation = (): Effect.Effect< const allOutputs = [...updatedState.outputs, ...buildCtx.changeOutputs] const transaction = yield* assembleTransaction(inputs, allOutputs, buildCtx.calculatedFee) - // Step 5: Serialize transaction to CBOR hex - const txCborBytes = yield* Effect.try({ - try: () => Transaction.toCBORBytes(transaction), - catch: (error) => - new TransactionBuilderError({ - message: "Failed to encode transaction to CBOR for evaluation", - cause: error - }) - }) - - const txHex = Bytes.toHex(txCborBytes) - // Debug: Log transaction details - yield* Effect.logDebug(`[Evaluation] Transaction CBOR length: ${txHex.length} chars`) + yield* Effect.logDebug(`[Evaluation] Transaction has ${transaction.body.inputs.length} inputs, ${transaction.body.outputs.length} outputs`) yield* Effect.logDebug(`[Evaluation] Has collateral return: ${!!transaction.body.collateralReturn}`) if (transaction.body.collateralReturn) { const assets = transaction.body.collateralReturn.assets @@ -570,15 +483,16 @@ export const executeEvaluation = (): Effect.Effect< ] const evalResults = yield* evaluator.evaluate( - txHex, + transaction, additionalUtxos, // UTxOs being spent + reference inputs (needed to resolve script hashes and datums) evaluationContext ).pipe( Effect.mapError( (evalError) => { - // Parse Ogmios error and enrich with labels - const failures = parseOgmiosError( - evalError, + // Enrich failures with labels from builder state + // evalError.failures contains raw failures from evaluator (provider or aiken) + const enrichedFailures = enrichFailuresWithLabels( + evalError.failures ?? [], updatedState.redeemers, inputIndexMapping, withdrawalIndexMapping, @@ -586,15 +500,15 @@ export const executeEvaluation = (): Effect.Effect< certIndexMapping ) - // Create enhanced evaluation error + // Create enhanced evaluation error with enriched failures const enhancedError = new EvaluationError({ message: "Script evaluation failed", - cause: evalError, - failures + cause: evalError.cause, + failures: enrichedFailures }) return new TransactionBuilderError({ - message: "Script evaluation failed", + message: `Script evaluation failed: ${evalError.message}`, cause: enhancedError }) } diff --git a/packages/evolution/src/sdk/builders/phases/FeeCalculation.ts b/packages/evolution/src/sdk/builders/phases/FeeCalculation.ts index b2180a2c..73a92fb4 100644 --- a/packages/evolution/src/sdk/builders/phases/FeeCalculation.ts +++ b/packages/evolution/src/sdk/builders/phases/FeeCalculation.ts @@ -12,7 +12,7 @@ import { Effect, Ref } from "effect" import * as CoreAssets from "../../../core/Assets/index.js" import type { TransactionBuilderError } from "../TransactionBuilder.js" -import { PhaseContextTag, ProtocolParametersTag, TxContext } from "../TransactionBuilder.js" +import { BuildOptionsTag, PhaseContextTag, ProtocolParametersTag, TxContext } from "../TransactionBuilder.js" import { buildTransactionInputs, calculateFeeIteratively, calculateReferenceScriptFee } from "../TxBuilderImpl.js" import type { PhaseResult } from "./Phases.js" @@ -54,7 +54,7 @@ import type { PhaseResult } from "./Phases.js" export const executeFeeCalculation = (): Effect.Effect< PhaseResult, TransactionBuilderError, - PhaseContextTag | TxContext | ProtocolParametersTag + PhaseContextTag | TxContext | ProtocolParametersTag | BuildOptionsTag > => Effect.gen(function* () { // Step 1: Get contexts and current state diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 70598f9b..d6abdc31 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -178,6 +178,19 @@ importers: specifier: ^5.9.2 version: 5.9.2 + packages/aiken-uplc: + dependencies: + '@evolution-sdk/evolution': + specifier: workspace:* + version: link:../evolution + effect: + specifier: ^3.19.3 + version: 3.19.3 + devDependencies: + typescript: + specifier: ^5.9.2 + version: 5.9.2 + packages/evolution: dependencies: '@effect/platform': @@ -241,6 +254,9 @@ importers: '@effect/platform-node': specifier: ^0.96.1 version: 0.96.1(@effect/cluster@0.48.2(@effect/platform@0.90.10(effect@3.19.3))(@effect/rpc@0.69.1(@effect/platform@0.90.10(effect@3.19.3))(effect@3.19.3))(@effect/sql@0.44.2(@effect/experimental@0.54.6(@effect/platform@0.90.10(effect@3.19.3))(effect@3.19.3))(@effect/platform@0.90.10(effect@3.19.3))(effect@3.19.3))(@effect/workflow@0.9.2(@effect/platform@0.90.10(effect@3.19.3))(@effect/rpc@0.69.1(@effect/platform@0.90.10(effect@3.19.3))(effect@3.19.3))(effect@3.19.3))(effect@3.19.3))(@effect/platform@0.90.10(effect@3.19.3))(@effect/rpc@0.69.1(@effect/platform@0.90.10(effect@3.19.3))(effect@3.19.3))(@effect/sql@0.44.2(@effect/experimental@0.54.6(@effect/platform@0.90.10(effect@3.19.3))(effect@3.19.3))(@effect/platform@0.90.10(effect@3.19.3))(effect@3.19.3))(effect@3.19.3) + '@evolution-sdk/aiken-uplc': + specifier: workspace:* + version: link:../aiken-uplc '@evolution-sdk/evolution': specifier: workspace:* version: link:../evolution