diff --git a/.changeset/cold-clubs-doubt.md b/.changeset/cold-clubs-doubt.md new file mode 100644 index 00000000..f6e663be --- /dev/null +++ b/.changeset/cold-clubs-doubt.md @@ -0,0 +1,11 @@ +--- +"@evolution-sdk/evolution": patch +--- + +Add transaction chaining support via `SignBuilder.chainResult()` + +- Add `chainResult()` method to `SignBuilder` for building dependent transactions +- Returns `ChainResult` with `consumed`, `available` UTxOs and pre-computed `txHash` +- Lazy evaluation with memoization - computed on first call, cached for subsequent calls +- Add `signAndSubmit()` convenience method combining sign and submit in one call +- Remove redundant `chain()`, `chainEffect()`, `chainEither()` methods from TransactionBuilder diff --git a/.changeset/common-ways-occur.md b/.changeset/common-ways-occur.md new file mode 100644 index 00000000..611d7b17 --- /dev/null +++ b/.changeset/common-ways-occur.md @@ -0,0 +1,18 @@ +--- +"@evolution-sdk/evolution": patch +--- + +### Native Scripts & Multi-Sig Support + +- **`addSigner` operation**: Add required signers to transactions for multi-sig and script validation +- **Native script minting**: Full support for `ScriptAll`, `ScriptAny`, `ScriptNOfK`, `InvalidBefore`, `InvalidHereafter` +- **Reference scripts**: Use native scripts via `readFrom` instead of attaching them to transactions +- **Multi-sig spending**: Spend from native script addresses with multi-party signing +- **Improved fee calculation**: Accurate fee estimation for transactions with native scripts and reference scripts + +### API Changes + +- `UTxO.scriptRef` type changed from `ScriptRef` to `Script` for better type safety +- `PayToAddressParams.scriptRef` renamed to `script` for consistency +- Wallet `signTx` now accepts `referenceUtxos` context for native script signer detection +- Client `signTx` auto-fetches reference UTxOs when signing transactions with reference inputs diff --git a/docs/content/docs/modules/core/NativeScripts.mdx b/docs/content/docs/modules/core/NativeScripts.mdx index 71ceb1d5..72e5baf0 100644 --- a/docs/content/docs/modules/core/NativeScripts.mdx +++ b/docs/content/docs/modules/core/NativeScripts.mdx @@ -43,6 +43,7 @@ parent: Modules - [arbitrary](#arbitrary) - [utilities](#utilities) - [countRequiredSigners](#countrequiredsigners) + - [extractKeyHashes](#extractkeyhashes) - [utils](#utils) - [CDDLSchema](#cddlschema) - [FromCBORBytes](#fromcborbytes-1) @@ -374,6 +375,19 @@ export declare const countRequiredSigners: (script: NativeScriptVariants) => num Added in v2.0.0 +## extractKeyHashes + +Extract all key hashes from a native script. +Recursively traverses nested scripts to find all ScriptPubKey key hashes. + +**Signature** + +```ts +export declare const extractKeyHashes: (script: NativeScriptVariants) => ReadonlyArray +``` + +Added in v2.0.0 + # utils ## CDDLSchema diff --git a/docs/content/docs/modules/core/UTxO.mdx b/docs/content/docs/modules/core/UTxO.mdx index 229cc80e..951333b8 100644 --- a/docs/content/docs/modules/core/UTxO.mdx +++ b/docs/content/docs/modules/core/UTxO.mdx @@ -25,8 +25,8 @@ parent: Modules - [datumOptionToSDK](#datumoptiontosdk) - [fromSDK](#fromsdk) - [fromSDKArray](#fromsdkarray) - - [scriptRefFromSDK](#scriptreffromsdk) - - [scriptRefToSDKHex](#scriptreftosdkhex) + - [scriptFromSDK](#scriptfromsdk) + - [scriptToSDK](#scripttosdk) - [toArray](#toarray) - [toSDK](#tosdk) - [toSDKArray](#tosdkarray) @@ -37,7 +37,6 @@ parent: Modules - [UTxO (class)](#utxo-class) - [toJSON (method)](#tojson-method) - [toString (method)](#tostring-method) - - [[Inspectable.NodeInspectSymbol] (method)](#inspectablenodeinspectsymbol-method) - [[Equal.symbol] (method)](#equalsymbol-method) - [[Hash.symbol] (method)](#hashsymbol-method) - [models](#models) @@ -207,29 +206,28 @@ export declare const fromSDKArray: ( Added in v2.0.0 -## scriptRefFromSDK +## scriptFromSDK -Convert SDK Script to Core ScriptRef. +Convert SDK Script to Core Script. **Signature** ```ts -export declare const scriptRefFromSDK: ( - script: SDKScript.Script -) => Effect.Effect +export declare const scriptFromSDK: ( + sdkScript: SDKScript.Script +) => Effect.Effect ``` Added in v2.0.0 -## scriptRefToSDKHex +## scriptToSDK -Convert Core ScriptRef to SDK Script type string. -Note: We lose the script type information as ScriptRef only stores bytes. +Convert Core Script to SDK Script. **Signature** ```ts -export declare const scriptRefToSDKHex: (scriptRef: ScriptRef.ScriptRef) => string +export declare const scriptToSDK: (script: Script.Script) => SDKScript.Script ``` Added in v2.0.0 @@ -332,14 +330,6 @@ toJSON() toString(): string ``` -### [Inspectable.NodeInspectSymbol] (method) - -**Signature** - -```ts -[Inspectable.NodeInspectSymbol](): unknown -``` - ### [Equal.symbol] (method) **Signature** diff --git a/docs/content/docs/modules/sdk/Credential.mdx b/docs/content/docs/modules/sdk/Credential.mdx index edf19c25..4c6e8b84 100644 --- a/docs/content/docs/modules/sdk/Credential.mdx +++ b/docs/content/docs/modules/sdk/Credential.mdx @@ -1,6 +1,6 @@ --- title: sdk/Credential.ts -nav_order: 180 +nav_order: 181 parent: Modules --- diff --git a/docs/content/docs/modules/sdk/Datum.mdx b/docs/content/docs/modules/sdk/Datum.mdx index d67da2b4..4f2c08e4 100644 --- a/docs/content/docs/modules/sdk/Datum.mdx +++ b/docs/content/docs/modules/sdk/Datum.mdx @@ -1,6 +1,6 @@ --- title: sdk/Datum.ts -nav_order: 181 +nav_order: 182 parent: Modules --- diff --git a/docs/content/docs/modules/sdk/Delegation.mdx b/docs/content/docs/modules/sdk/Delegation.mdx index a28949b0..0af89451 100644 --- a/docs/content/docs/modules/sdk/Delegation.mdx +++ b/docs/content/docs/modules/sdk/Delegation.mdx @@ -1,6 +1,6 @@ --- title: sdk/Delegation.ts -nav_order: 182 +nav_order: 183 parent: Modules --- diff --git a/docs/content/docs/modules/sdk/EvalRedeemer.mdx b/docs/content/docs/modules/sdk/EvalRedeemer.mdx index df8cf48a..9a5c6602 100644 --- a/docs/content/docs/modules/sdk/EvalRedeemer.mdx +++ b/docs/content/docs/modules/sdk/EvalRedeemer.mdx @@ -1,6 +1,6 @@ --- title: sdk/EvalRedeemer.ts -nav_order: 183 +nav_order: 184 parent: Modules --- diff --git a/docs/content/docs/modules/sdk/Network.mdx b/docs/content/docs/modules/sdk/Network.mdx index 91a6e808..864ecacb 100644 --- a/docs/content/docs/modules/sdk/Network.mdx +++ b/docs/content/docs/modules/sdk/Network.mdx @@ -1,6 +1,6 @@ --- title: sdk/Network.ts -nav_order: 184 +nav_order: 185 parent: Modules --- diff --git a/docs/content/docs/modules/sdk/OutRef.mdx b/docs/content/docs/modules/sdk/OutRef.mdx index c0df0362..3c4dfd0a 100644 --- a/docs/content/docs/modules/sdk/OutRef.mdx +++ b/docs/content/docs/modules/sdk/OutRef.mdx @@ -1,6 +1,6 @@ --- title: sdk/OutRef.ts -nav_order: 185 +nav_order: 186 parent: Modules --- diff --git a/docs/content/docs/modules/sdk/PolicyId.mdx b/docs/content/docs/modules/sdk/PolicyId.mdx index 6f22a185..172eed69 100644 --- a/docs/content/docs/modules/sdk/PolicyId.mdx +++ b/docs/content/docs/modules/sdk/PolicyId.mdx @@ -1,6 +1,6 @@ --- title: sdk/PolicyId.ts -nav_order: 186 +nav_order: 187 parent: Modules --- diff --git a/docs/content/docs/modules/sdk/PoolParams.mdx b/docs/content/docs/modules/sdk/PoolParams.mdx index dcdccbb0..6eed6fbf 100644 --- a/docs/content/docs/modules/sdk/PoolParams.mdx +++ b/docs/content/docs/modules/sdk/PoolParams.mdx @@ -1,6 +1,6 @@ --- title: sdk/PoolParams.ts -nav_order: 187 +nav_order: 188 parent: Modules --- diff --git a/docs/content/docs/modules/sdk/ProtocolParameters.mdx b/docs/content/docs/modules/sdk/ProtocolParameters.mdx index 7456cbe4..85d7b42b 100644 --- a/docs/content/docs/modules/sdk/ProtocolParameters.mdx +++ b/docs/content/docs/modules/sdk/ProtocolParameters.mdx @@ -1,6 +1,6 @@ --- title: sdk/ProtocolParameters.ts -nav_order: 188 +nav_order: 189 parent: Modules --- diff --git a/docs/content/docs/modules/sdk/Relay.mdx b/docs/content/docs/modules/sdk/Relay.mdx index 13b01b2b..5aeb464f 100644 --- a/docs/content/docs/modules/sdk/Relay.mdx +++ b/docs/content/docs/modules/sdk/Relay.mdx @@ -1,6 +1,6 @@ --- title: sdk/Relay.ts -nav_order: 194 +nav_order: 195 parent: Modules --- diff --git a/docs/content/docs/modules/sdk/RewardAddress.mdx b/docs/content/docs/modules/sdk/RewardAddress.mdx index 3cd77568..f42e2c47 100644 --- a/docs/content/docs/modules/sdk/RewardAddress.mdx +++ b/docs/content/docs/modules/sdk/RewardAddress.mdx @@ -1,6 +1,6 @@ --- title: sdk/RewardAddress.ts -nav_order: 195 +nav_order: 196 parent: Modules --- diff --git a/docs/content/docs/modules/sdk/Script.mdx b/docs/content/docs/modules/sdk/Script.mdx index 48002b3d..8cc3b594 100644 --- a/docs/content/docs/modules/sdk/Script.mdx +++ b/docs/content/docs/modules/sdk/Script.mdx @@ -1,6 +1,6 @@ --- title: sdk/Script.ts -nav_order: 196 +nav_order: 197 parent: Modules --- diff --git a/docs/content/docs/modules/sdk/Type.mdx b/docs/content/docs/modules/sdk/Type.mdx index 48ff5a27..a4bdd761 100644 --- a/docs/content/docs/modules/sdk/Type.mdx +++ b/docs/content/docs/modules/sdk/Type.mdx @@ -1,6 +1,6 @@ --- title: sdk/Type.ts -nav_order: 197 +nav_order: 198 parent: Modules --- diff --git a/docs/content/docs/modules/sdk/UTxO.mdx b/docs/content/docs/modules/sdk/UTxO.mdx index 894fa130..706a64b8 100644 --- a/docs/content/docs/modules/sdk/UTxO.mdx +++ b/docs/content/docs/modules/sdk/UTxO.mdx @@ -1,6 +1,6 @@ --- title: sdk/UTxO.ts -nav_order: 199 +nav_order: 200 parent: Modules --- diff --git a/docs/content/docs/modules/sdk/Unit.mdx b/docs/content/docs/modules/sdk/Unit.mdx index 65395016..fee5e86f 100644 --- a/docs/content/docs/modules/sdk/Unit.mdx +++ b/docs/content/docs/modules/sdk/Unit.mdx @@ -1,6 +1,6 @@ --- title: sdk/Unit.ts -nav_order: 198 +nav_order: 199 parent: Modules --- diff --git a/docs/content/docs/modules/sdk/builders/RedeemerBuilder.mdx b/docs/content/docs/modules/sdk/builders/RedeemerBuilder.mdx index b2a7c1ff..dac49194 100644 --- a/docs/content/docs/modules/sdk/builders/RedeemerBuilder.mdx +++ b/docs/content/docs/modules/sdk/builders/RedeemerBuilder.mdx @@ -1,6 +1,6 @@ --- title: sdk/builders/RedeemerBuilder.ts -nav_order: 169 +nav_order: 170 parent: Modules --- diff --git a/docs/content/docs/modules/sdk/builders/SignBuilder.mdx b/docs/content/docs/modules/sdk/builders/SignBuilder.mdx index 02b6fc8c..52d50564 100644 --- a/docs/content/docs/modules/sdk/builders/SignBuilder.mdx +++ b/docs/content/docs/modules/sdk/builders/SignBuilder.mdx @@ -1,6 +1,6 @@ --- title: sdk/builders/SignBuilder.ts -nav_order: 170 +nav_order: 171 parent: Modules --- @@ -25,11 +25,21 @@ SignBuilder extends TransactionResultBase with signing capabilities. Only available when the client has a signing wallet (seed, private key, or API wallet). Provides access to unsigned transaction (via base interface) and signing operations. +Includes `chainResult` for transaction chaining - use `chainResult.available` as +`availableUtxos` for the next transaction in a chain. + **Signature** ```ts export interface SignBuilder extends TransactionResultBase, EffectToPromiseAPI { readonly Effect: SignBuilderEffect + /** + * Compute chain result for building dependent transactions. + * Contains consumed UTxOs, available UTxOs (remaining + created), and txHash. + * + * Result is memoized - computed once on first call, cached for subsequent calls. + */ + readonly chainResult: () => ChainResult } ``` @@ -52,6 +62,7 @@ export interface SignBuilderEffect { // Signing methods readonly sign: () => Effect.Effect + readonly signAndSubmit: () => Effect.Effect readonly signWithWitness: ( witnessSet: TransactionWitnessSet.TransactionWitnessSet ) => Effect.Effect diff --git a/docs/content/docs/modules/sdk/builders/SignBuilderImpl.mdx b/docs/content/docs/modules/sdk/builders/SignBuilderImpl.mdx index b936ec75..f2706322 100644 --- a/docs/content/docs/modules/sdk/builders/SignBuilderImpl.mdx +++ b/docs/content/docs/modules/sdk/builders/SignBuilderImpl.mdx @@ -1,6 +1,6 @@ --- title: sdk/builders/SignBuilderImpl.ts -nav_order: 171 +nav_order: 172 parent: Modules --- @@ -43,8 +43,11 @@ export declare const makeSignBuilder: (params: { transactionWithFakeWitnesses: Transaction.Transaction fee: bigint utxos: ReadonlyArray + referenceUtxos: ReadonlyArray provider: Provider.Provider wallet: Wallet + outputs: ReadonlyArray + availableUtxos: ReadonlyArray }) => SignBuilder ``` diff --git a/docs/content/docs/modules/sdk/builders/SubmitBuilder.mdx b/docs/content/docs/modules/sdk/builders/SubmitBuilder.mdx index 2dc07f84..88a5622a 100644 --- a/docs/content/docs/modules/sdk/builders/SubmitBuilder.mdx +++ b/docs/content/docs/modules/sdk/builders/SubmitBuilder.mdx @@ -1,6 +1,6 @@ --- title: sdk/builders/SubmitBuilder.ts -nav_order: 172 +nav_order: 173 parent: Modules --- diff --git a/docs/content/docs/modules/sdk/builders/SubmitBuilderImpl.mdx b/docs/content/docs/modules/sdk/builders/SubmitBuilderImpl.mdx index 69b40659..3026a8d7 100644 --- a/docs/content/docs/modules/sdk/builders/SubmitBuilderImpl.mdx +++ b/docs/content/docs/modules/sdk/builders/SubmitBuilderImpl.mdx @@ -1,6 +1,6 @@ --- title: sdk/builders/SubmitBuilderImpl.ts -nav_order: 173 +nav_order: 174 parent: Modules --- diff --git a/docs/content/docs/modules/sdk/builders/TransactionBuilder.mdx b/docs/content/docs/modules/sdk/builders/TransactionBuilder.mdx index 023ecd3e..7d8f6b91 100644 --- a/docs/content/docs/modules/sdk/builders/TransactionBuilder.mdx +++ b/docs/content/docs/modules/sdk/builders/TransactionBuilder.mdx @@ -1,6 +1,6 @@ --- title: sdk/builders/TransactionBuilder.ts -nav_order: 174 +nav_order: 175 parent: Modules --- @@ -594,6 +594,71 @@ export interface TransactionBuilderBase { * @category validity-methods */ readonly setValidity: (params: ValidityParams) => this + + /** + * Add a required signer to the transaction. + * + * Adds a key hash to the transaction's requiredSigners field. This is used to + * require specific key signatures even when those keys don't control inputs. + * Common use cases include: + * - Multi-sig schemes requiring explicit signature verification + * - Plutus scripts that check for specific signers in the transaction + * - Governance transactions requiring DRep or committee member signatures + * + * Duplicate key hashes are automatically deduplicated. + * + * Queues a deferred operation that will be executed when build() is called. + * Returns the same builder for method chaining. + * + * @example + * ```typescript + * import * as KeyHash from "@evolution-sdk/core/KeyHash" + * import * as Address from "@evolution-sdk/core/Address" + * + * // Add signer from address credential + * const address = Address.fromBech32("addr_test1...") + * const cred = address.paymentCredential + * if (cred._tag === "KeyHash") { + * const tx = await builder + * .addSigner({ keyHash: cred }) + * .build() + * } + * ``` + * + * @since 2.0.0 + * @category builder-methods + */ + readonly addSigner: (params: AddSignerParams) => this + + // ============================================================================ + // Transaction Chaining Methods + // ============================================================================ + + /** + * Execute transaction build and return consumed/available UTxOs for chaining. + * + * Runs the full build pipeline (coin selection, fee calculation, evaluation) and returns + * which UTxOs were consumed and which remain available for subsequent transactions. + * Use this when building multiple dependent transactions in sequence. + * + * @returns Promise with consumed and available UTxOs + * + * @example + * ```typescript + * // Build first transaction, get remaining UTxOs + * const tx1 = await builder + * .payTo({ address, value: { lovelace: 5_000_000n } }) + * .build({ availableUtxos: walletUtxos }) + * + * // Build second transaction using remaining UTxOs from chainResult + * const tx2 = await builder + * .payTo({ address, value: { lovelace: 3_000_000n } }) + * .build({ availableUtxos: tx1.chainResult().available }) + * ``` + * + * @since 2.0.0 + * @category chaining-methods + */ } ```` @@ -986,17 +1051,22 @@ Added in v2.0.0 Result type for transaction chaining operations. -**NOTE: NOT YET IMPLEMENTED** - This interface is reserved for future implementation -of multi-transaction workflows. Current chain methods return stub implementations. +Provides consumed and available UTxOs for building chained transactions. +The available UTxOs include both remaining unspent inputs AND newly created outputs +with pre-computed txHash, ready to be spent in subsequent transactions. + +Accessed via `SignBuilder.chainResult()` after calling `build()`. **Signature** ```ts export interface ChainResult { - readonly transaction: Transaction.Transaction - readonly newOutputs: ReadonlyArray // UTxOs created by this transaction - readonly updatedUtxos: ReadonlyArray // Available UTxOs for next transaction (original - spent + new) - readonly spentUtxos: ReadonlyArray // UTxOs consumed by this transaction + /** UTxOs consumed from availableUtxos by coin selection */ + readonly consumed: ReadonlyArray + /** Available UTxOs: remaining unspent + newly created (with computed txHash) */ + readonly available: ReadonlyArray + /** Pre-computed transaction hash (blake2b-256 of transaction body) */ + readonly txHash: string } ``` @@ -1134,6 +1204,7 @@ export interface TxBuilderState { readonly from?: Time.UnixTime // validityIntervalStart readonly to?: Time.UnixTime // ttl } + readonly requiredSigners: ReadonlyArray // Extra signers required (for script validation) } ``` diff --git a/docs/content/docs/modules/sdk/builders/TransactionResult.mdx b/docs/content/docs/modules/sdk/builders/TransactionResult.mdx index 8bca4361..3c2927a6 100644 --- a/docs/content/docs/modules/sdk/builders/TransactionResult.mdx +++ b/docs/content/docs/modules/sdk/builders/TransactionResult.mdx @@ -1,6 +1,6 @@ --- title: sdk/builders/TransactionResult.ts -nav_order: 175 +nav_order: 176 parent: Modules --- diff --git a/docs/content/docs/modules/sdk/builders/TxBuilderImpl.mdx b/docs/content/docs/modules/sdk/builders/TxBuilderImpl.mdx index 67e0482d..2b80a83c 100644 --- a/docs/content/docs/modules/sdk/builders/TxBuilderImpl.mdx +++ b/docs/content/docs/modules/sdk/builders/TxBuilderImpl.mdx @@ -1,6 +1,6 @@ --- title: sdk/builders/TxBuilderImpl.ts -nav_order: 176 +nav_order: 177 parent: Modules --- @@ -62,7 +62,7 @@ export declare const assembleTransaction: ( inputs: ReadonlyArray, outputs: ReadonlyArray, fee: bigint -) => Effect.Effect +) => Effect.Effect ``` Added in v2.0.0 @@ -100,7 +100,7 @@ export declare const calculateMinimumUtxoLovelace: (params: { address: CoreAddress.Address assets: CoreAssets.Assets datum?: DatumOption.DatumOption - scriptRef?: TxOut.TransactionOutput["scriptRef"] + scriptRef?: CoreScript.Script coinsPerUtxoByte: bigint }) => Effect.Effect ``` @@ -349,7 +349,7 @@ export declare const makeTxOutput: (params: { address: CoreAddress.Address assets: CoreAssets.Assets datum?: DatumOption.DatumOption - scriptRef?: TxOut.TransactionOutput["scriptRef"] + scriptRef?: CoreScript.Script }) => Effect.Effect ``` diff --git a/docs/content/docs/modules/sdk/builders/Unfrack.mdx b/docs/content/docs/modules/sdk/builders/Unfrack.mdx index a067138f..e6c5ef7a 100644 --- a/docs/content/docs/modules/sdk/builders/Unfrack.mdx +++ b/docs/content/docs/modules/sdk/builders/Unfrack.mdx @@ -1,6 +1,6 @@ --- title: sdk/builders/Unfrack.ts -nav_order: 177 +nav_order: 178 parent: Modules --- diff --git a/docs/content/docs/modules/sdk/builders/operations/AddSigner.mdx b/docs/content/docs/modules/sdk/builders/operations/AddSigner.mdx new file mode 100644 index 00000000..5fb66bc8 --- /dev/null +++ b/docs/content/docs/modules/sdk/builders/operations/AddSigner.mdx @@ -0,0 +1,44 @@ +--- +title: sdk/builders/operations/AddSigner.ts +nav_order: 153 +parent: Modules +--- + +## AddSigner overview + +AddSigner operation - adds required signers to the transaction. + +Required signers are key hashes that must sign the transaction even if they +don't control any inputs. This is commonly used for scripts that check for +specific signers in their validation logic. + +Added in v2.0.0 + +--- + +

Table of contents

+ +- [programs](#programs) + - [createAddSignerProgram](#createaddsignerprogram) + +--- + +# programs + +## createAddSignerProgram + +Creates a ProgramStep for addSigner operation. +Adds a required signer (key hash) to the transaction. + +Implementation: + +1. Adds the key hash to the requiredSigners array in state +2. Deduplicates to avoid duplicate signers + +**Signature** + +```ts +export declare const createAddSignerProgram: (params: AddSignerParams) => 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 24f62e44..5888e7f0 100644 --- a/docs/content/docs/modules/sdk/builders/operations/Attach.mdx +++ b/docs/content/docs/modules/sdk/builders/operations/Attach.mdx @@ -1,6 +1,6 @@ --- title: sdk/builders/operations/Attach.ts -nav_order: 153 +nav_order: 154 parent: Modules --- diff --git a/docs/content/docs/modules/sdk/builders/operations/Collect.mdx b/docs/content/docs/modules/sdk/builders/operations/Collect.mdx index 6f66b790..58184cbf 100644 --- a/docs/content/docs/modules/sdk/builders/operations/Collect.mdx +++ b/docs/content/docs/modules/sdk/builders/operations/Collect.mdx @@ -1,6 +1,6 @@ --- title: sdk/builders/operations/Collect.ts -nav_order: 154 +nav_order: 155 parent: Modules --- diff --git a/docs/content/docs/modules/sdk/builders/operations/Mint.mdx b/docs/content/docs/modules/sdk/builders/operations/Mint.mdx index 4fcc6a92..5127b857 100644 --- a/docs/content/docs/modules/sdk/builders/operations/Mint.mdx +++ b/docs/content/docs/modules/sdk/builders/operations/Mint.mdx @@ -1,6 +1,6 @@ --- title: sdk/builders/operations/Mint.ts -nav_order: 155 +nav_order: 156 parent: Modules --- diff --git a/docs/content/docs/modules/sdk/builders/operations/Operations.mdx b/docs/content/docs/modules/sdk/builders/operations/Operations.mdx index 5bf99e6f..e1ba7b8e 100644 --- a/docs/content/docs/modules/sdk/builders/operations/Operations.mdx +++ b/docs/content/docs/modules/sdk/builders/operations/Operations.mdx @@ -1,6 +1,6 @@ --- title: sdk/builders/operations/Operations.ts -nav_order: 156 +nav_order: 157 parent: Modules --- @@ -19,6 +19,8 @@ parent: Modules - [pool](#pool) - [RegisterPoolParams (interface)](#registerpoolparams-interface) - [RetirePoolParams (interface)](#retirepoolparams-interface) +- [signers](#signers) + - [AddSignerParams (interface)](#addsignerparams-interface) - [staking](#staking) - [DelegateToParams (interface)](#delegatetoparams-interface) - [DeregisterStakeParams (interface)](#deregisterstakeparams-interface) @@ -180,6 +182,26 @@ export interface RetirePoolParams { Added in v2.0.0 +# signers + +## AddSignerParams (interface) + +Parameters for adding a required signer to the transaction. + +Required signers must sign the transaction even if they don't control any inputs. +This is commonly used for scripts that check for specific signers in their validation logic. + +**Signature** + +```ts +export interface AddSignerParams { + /** The key hash that must sign the transaction */ + readonly keyHash: KeyHash.KeyHash +} +``` + +Added in v2.0.0 + # staking ## DelegateToParams (interface) @@ -367,7 +389,8 @@ export interface PayToAddressParams { readonly address: CoreAddress.Address readonly assets: CoreAssets.Assets readonly datum?: CoreDatumOption.DatumOption - readonly scriptRef?: CoreScriptRef.ScriptRef + /** Optional script to store as a reference script in the output */ + readonly script?: CoreScript.Script } ``` diff --git a/docs/content/docs/modules/sdk/builders/operations/Pay.mdx b/docs/content/docs/modules/sdk/builders/operations/Pay.mdx index df86f69c..49f6bf59 100644 --- a/docs/content/docs/modules/sdk/builders/operations/Pay.mdx +++ b/docs/content/docs/modules/sdk/builders/operations/Pay.mdx @@ -1,6 +1,6 @@ --- title: sdk/builders/operations/Pay.ts -nav_order: 157 +nav_order: 158 parent: Modules --- diff --git a/docs/content/docs/modules/sdk/builders/operations/ReadFrom.mdx b/docs/content/docs/modules/sdk/builders/operations/ReadFrom.mdx index 7a5060f4..18fa63c7 100644 --- a/docs/content/docs/modules/sdk/builders/operations/ReadFrom.mdx +++ b/docs/content/docs/modules/sdk/builders/operations/ReadFrom.mdx @@ -1,6 +1,6 @@ --- title: sdk/builders/operations/ReadFrom.ts -nav_order: 158 +nav_order: 159 parent: Modules --- diff --git a/docs/content/docs/modules/sdk/builders/operations/Stake.mdx b/docs/content/docs/modules/sdk/builders/operations/Stake.mdx index aed8911c..67b761a0 100644 --- a/docs/content/docs/modules/sdk/builders/operations/Stake.mdx +++ b/docs/content/docs/modules/sdk/builders/operations/Stake.mdx @@ -1,6 +1,6 @@ --- title: sdk/builders/operations/Stake.ts -nav_order: 159 +nav_order: 160 parent: Modules --- diff --git a/docs/content/docs/modules/sdk/builders/operations/Validity.mdx b/docs/content/docs/modules/sdk/builders/operations/Validity.mdx index d355aba2..1e6f2d06 100644 --- a/docs/content/docs/modules/sdk/builders/operations/Validity.mdx +++ b/docs/content/docs/modules/sdk/builders/operations/Validity.mdx @@ -1,6 +1,6 @@ --- title: sdk/builders/operations/Validity.ts -nav_order: 160 +nav_order: 161 parent: Modules --- diff --git a/docs/content/docs/modules/sdk/builders/phases/Balance.mdx b/docs/content/docs/modules/sdk/builders/phases/Balance.mdx index 0a180cb7..2d7340de 100644 --- a/docs/content/docs/modules/sdk/builders/phases/Balance.mdx +++ b/docs/content/docs/modules/sdk/builders/phases/Balance.mdx @@ -1,6 +1,6 @@ --- title: sdk/builders/phases/Balance.ts -nav_order: 161 +nav_order: 162 parent: Modules --- diff --git a/docs/content/docs/modules/sdk/builders/phases/ChangeCreation.mdx b/docs/content/docs/modules/sdk/builders/phases/ChangeCreation.mdx index 4f8695cc..64553511 100644 --- a/docs/content/docs/modules/sdk/builders/phases/ChangeCreation.mdx +++ b/docs/content/docs/modules/sdk/builders/phases/ChangeCreation.mdx @@ -1,6 +1,6 @@ --- title: sdk/builders/phases/ChangeCreation.ts -nav_order: 162 +nav_order: 163 parent: Modules --- diff --git a/docs/content/docs/modules/sdk/builders/phases/Collateral.mdx b/docs/content/docs/modules/sdk/builders/phases/Collateral.mdx index dcc0c5fa..fe8855a0 100644 --- a/docs/content/docs/modules/sdk/builders/phases/Collateral.mdx +++ b/docs/content/docs/modules/sdk/builders/phases/Collateral.mdx @@ -1,6 +1,6 @@ --- title: sdk/builders/phases/Collateral.ts -nav_order: 163 +nav_order: 164 parent: Modules --- diff --git a/docs/content/docs/modules/sdk/builders/phases/Evaluation.mdx b/docs/content/docs/modules/sdk/builders/phases/Evaluation.mdx index dd25b726..3894c0a8 100644 --- a/docs/content/docs/modules/sdk/builders/phases/Evaluation.mdx +++ b/docs/content/docs/modules/sdk/builders/phases/Evaluation.mdx @@ -1,6 +1,6 @@ --- title: sdk/builders/phases/Evaluation.ts -nav_order: 164 +nav_order: 165 parent: Modules --- diff --git a/docs/content/docs/modules/sdk/builders/phases/Fallback.mdx b/docs/content/docs/modules/sdk/builders/phases/Fallback.mdx index 7df3aeb1..7ed4887d 100644 --- a/docs/content/docs/modules/sdk/builders/phases/Fallback.mdx +++ b/docs/content/docs/modules/sdk/builders/phases/Fallback.mdx @@ -1,6 +1,6 @@ --- title: sdk/builders/phases/Fallback.ts -nav_order: 165 +nav_order: 166 parent: Modules --- diff --git a/docs/content/docs/modules/sdk/builders/phases/FeeCalculation.mdx b/docs/content/docs/modules/sdk/builders/phases/FeeCalculation.mdx index 1b032b4a..3e930312 100644 --- a/docs/content/docs/modules/sdk/builders/phases/FeeCalculation.mdx +++ b/docs/content/docs/modules/sdk/builders/phases/FeeCalculation.mdx @@ -1,6 +1,6 @@ --- title: sdk/builders/phases/FeeCalculation.ts -nav_order: 166 +nav_order: 167 parent: Modules --- diff --git a/docs/content/docs/modules/sdk/builders/phases/Phases.mdx b/docs/content/docs/modules/sdk/builders/phases/Phases.mdx index 8e26829b..7ae22c52 100644 --- a/docs/content/docs/modules/sdk/builders/phases/Phases.mdx +++ b/docs/content/docs/modules/sdk/builders/phases/Phases.mdx @@ -1,6 +1,6 @@ --- title: sdk/builders/phases/Phases.ts -nav_order: 167 +nav_order: 168 parent: Modules --- diff --git a/docs/content/docs/modules/sdk/builders/phases/Selection.mdx b/docs/content/docs/modules/sdk/builders/phases/Selection.mdx index f58fe38e..780bc221 100644 --- a/docs/content/docs/modules/sdk/builders/phases/Selection.mdx +++ b/docs/content/docs/modules/sdk/builders/phases/Selection.mdx @@ -1,6 +1,6 @@ --- title: sdk/builders/phases/Selection.ts -nav_order: 168 +nav_order: 169 parent: Modules --- diff --git a/docs/content/docs/modules/sdk/client/Client.mdx b/docs/content/docs/modules/sdk/client/Client.mdx index 9aa6b973..0cfa5ad1 100644 --- a/docs/content/docs/modules/sdk/client/Client.mdx +++ b/docs/content/docs/modules/sdk/client/Client.mdx @@ -1,6 +1,6 @@ --- title: sdk/client/Client.ts -nav_order: 178 +nav_order: 179 parent: Modules --- diff --git a/docs/content/docs/modules/sdk/client/ClientImpl.mdx b/docs/content/docs/modules/sdk/client/ClientImpl.mdx index 6234868f..e6dfa93c 100644 --- a/docs/content/docs/modules/sdk/client/ClientImpl.mdx +++ b/docs/content/docs/modules/sdk/client/ClientImpl.mdx @@ -1,6 +1,6 @@ --- title: sdk/client/ClientImpl.ts -nav_order: 179 +nav_order: 180 parent: Modules --- diff --git a/docs/content/docs/modules/sdk/provider/Blockfrost.mdx b/docs/content/docs/modules/sdk/provider/Blockfrost.mdx index 9e2eb18a..4a2900c1 100644 --- a/docs/content/docs/modules/sdk/provider/Blockfrost.mdx +++ b/docs/content/docs/modules/sdk/provider/Blockfrost.mdx @@ -1,6 +1,6 @@ --- title: sdk/provider/Blockfrost.ts -nav_order: 189 +nav_order: 190 parent: Modules --- diff --git a/docs/content/docs/modules/sdk/provider/Koios.mdx b/docs/content/docs/modules/sdk/provider/Koios.mdx index cefe7465..62e4d29c 100644 --- a/docs/content/docs/modules/sdk/provider/Koios.mdx +++ b/docs/content/docs/modules/sdk/provider/Koios.mdx @@ -1,6 +1,6 @@ --- title: sdk/provider/Koios.ts -nav_order: 190 +nav_order: 191 parent: Modules --- diff --git a/docs/content/docs/modules/sdk/provider/Kupmios.mdx b/docs/content/docs/modules/sdk/provider/Kupmios.mdx index 2f0f754b..6f81ab63 100644 --- a/docs/content/docs/modules/sdk/provider/Kupmios.mdx +++ b/docs/content/docs/modules/sdk/provider/Kupmios.mdx @@ -1,6 +1,6 @@ --- title: sdk/provider/Kupmios.ts -nav_order: 191 +nav_order: 192 parent: Modules --- diff --git a/docs/content/docs/modules/sdk/provider/Maestro.mdx b/docs/content/docs/modules/sdk/provider/Maestro.mdx index e02b8394..980983ef 100644 --- a/docs/content/docs/modules/sdk/provider/Maestro.mdx +++ b/docs/content/docs/modules/sdk/provider/Maestro.mdx @@ -1,6 +1,6 @@ --- title: sdk/provider/Maestro.ts -nav_order: 192 +nav_order: 193 parent: Modules --- diff --git a/docs/content/docs/modules/sdk/provider/Provider.mdx b/docs/content/docs/modules/sdk/provider/Provider.mdx index 051131b7..e932ae9a 100644 --- a/docs/content/docs/modules/sdk/provider/Provider.mdx +++ b/docs/content/docs/modules/sdk/provider/Provider.mdx @@ -1,6 +1,6 @@ --- title: sdk/provider/Provider.ts -nav_order: 193 +nav_order: 194 parent: Modules --- diff --git a/docs/content/docs/modules/sdk/wallet/Derivation.mdx b/docs/content/docs/modules/sdk/wallet/Derivation.mdx index 9a97b97b..76ff9107 100644 --- a/docs/content/docs/modules/sdk/wallet/Derivation.mdx +++ b/docs/content/docs/modules/sdk/wallet/Derivation.mdx @@ -1,6 +1,6 @@ --- title: sdk/wallet/Derivation.ts -nav_order: 200 +nav_order: 201 parent: Modules --- diff --git a/docs/content/docs/modules/sdk/wallet/WalletNew.mdx b/docs/content/docs/modules/sdk/wallet/WalletNew.mdx index 1202c838..47c9bc49 100644 --- a/docs/content/docs/modules/sdk/wallet/WalletNew.mdx +++ b/docs/content/docs/modules/sdk/wallet/WalletNew.mdx @@ -1,6 +1,6 @@ --- title: sdk/wallet/WalletNew.ts -nav_order: 201 +nav_order: 202 parent: Modules --- @@ -139,7 +139,7 @@ API wallets handle both signing and submission through the wallet extension. export interface ApiWalletEffect extends ReadOnlyWalletEffect { readonly signTx: ( tx: Transaction.Transaction | string, - context?: { utxos?: ReadonlyArray } + context?: { utxos?: ReadonlyArray; referenceUtxos?: ReadonlyArray } ) => Effect.Effect readonly signMessage: ( address: CoreAddress.Address | RewardAddress.RewardAddress, @@ -255,11 +255,12 @@ export interface SigningWalletEffect extends ReadOnlyWalletEffect { /** * Sign a transaction given its structured representation. UTxOs required for correctness * (e.g. to determine required signers) must be supplied by the caller (client) and not - * fetched internally. + * fetched internally. Reference UTxOs are used to extract required signers from native scripts + * that are used via reference inputs. */ readonly signTx: ( tx: Transaction.Transaction | string, - context?: { utxos?: ReadonlyArray } + context?: { utxos?: ReadonlyArray; referenceUtxos?: ReadonlyArray } ) => Effect.Effect readonly signMessage: ( address: CoreAddress.Address | RewardAddress.RewardAddress, diff --git a/docs/content/docs/modules/utils/FeeValidation.mdx b/docs/content/docs/modules/utils/FeeValidation.mdx index b175df81..f0506d85 100644 --- a/docs/content/docs/modules/utils/FeeValidation.mdx +++ b/docs/content/docs/modules/utils/FeeValidation.mdx @@ -1,6 +1,6 @@ --- title: utils/FeeValidation.ts -nav_order: 203 +nav_order: 204 parent: Modules --- diff --git a/docs/content/docs/modules/utils/Hash.mdx b/docs/content/docs/modules/utils/Hash.mdx index 4eb5fd92..e90e746a 100644 --- a/docs/content/docs/modules/utils/Hash.mdx +++ b/docs/content/docs/modules/utils/Hash.mdx @@ -1,6 +1,6 @@ --- title: utils/Hash.ts -nav_order: 204 +nav_order: 205 parent: Modules --- diff --git a/docs/content/docs/modules/utils/effect-runtime.mdx b/docs/content/docs/modules/utils/effect-runtime.mdx index 0a0c897d..9626a09c 100644 --- a/docs/content/docs/modules/utils/effect-runtime.mdx +++ b/docs/content/docs/modules/utils/effect-runtime.mdx @@ -1,6 +1,6 @@ --- title: utils/effect-runtime.ts -nav_order: 202 +nav_order: 203 parent: Modules --- diff --git a/packages/evolution-devnet/test/TxBuilder.AddSigner.test.ts b/packages/evolution-devnet/test/TxBuilder.AddSigner.test.ts new file mode 100644 index 00000000..273e3cc1 --- /dev/null +++ b/packages/evolution-devnet/test/TxBuilder.AddSigner.test.ts @@ -0,0 +1,180 @@ +/** + * Devnet tests for TxBuilder addSigner operation. + * + * Tests the addSigner operation which adds required signers (key hashes) + * to the transaction body's requiredSigners field. + */ + +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 * as KeyHash from "@evolution-sdk/evolution/core/KeyHash" +import { createClient } from "@evolution-sdk/evolution/sdk/client/ClientImpl" + +describe("TxBuilder addSigner (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:1449", + ogmiosUrl: "http://localhost:1344" + }, + 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: "addsigner-test", + ports: { node: 6007, submit: 9008 }, + shelleyGenesis: genesisConfig, + kupo: { enabled: true, port: 1449, logLevel: "Info" }, + ogmios: { enabled: true, port: 1344, 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 include requiredSigners in transaction body and submit successfully", { timeout: 60_000 }, async () => { + const client = createTestClient(0) + const myAddress = await client.address() + + // Extract payment key hash from address credential + const paymentCredential = myAddress.paymentCredential + if (paymentCredential._tag !== "KeyHash") { + throw new Error("Expected KeyHash credential") + } + + const signBuilder = await client + .newTx() + .addSigner({ keyHash: paymentCredential }) + .payToAddress({ + address: myAddress, + assets: Core.Assets.fromLovelace(5_000_000n) + }) + .build({ availableUtxos: [...genesisUtxos] }) + + const tx = await signBuilder.toTransaction() + + // Verify requiredSigners is set + expect(tx.body.requiredSigners).toBeDefined() + expect(tx.body.requiredSigners?.length).toBe(1) + expect(tx.body.requiredSigners?.[0]._tag).toBe("KeyHash") + expect(KeyHash.toHex(tx.body.requiredSigners![0])).toBe(KeyHash.toHex(paymentCredential)) + + // Submit and verify confirmation + 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 support multi-sig with partial signing and assembly", { timeout: 60_000 }, async () => { + // Create two clients with different account indices (different key pairs) + const client1 = createTestClient(0) + const client2 = createTestClient(1) + + const address1 = await client1.address() + const address2 = await client2.address() + + // Extract payment key hashes from both addresses + const credential1 = address1.paymentCredential + const credential2 = address2.paymentCredential + + if (credential1._tag !== "KeyHash" || credential2._tag !== "KeyHash") { + throw new Error("Expected KeyHash credentials") + } + + // Fetch fresh UTxOs from the provider (after first test has run) + const freshUtxos = await client1.getUtxos(address1) + + // Build a transaction requiring BOTH signers + // Client1 builds and pays to self, but we require both keys + const signBuilder = await client1 + .newTx() + .addSigner({ keyHash: credential1 }) + .addSigner({ keyHash: credential2 }) + .payToAddress({ + address: address1, + assets: Core.Assets.fromLovelace(5_000_000n) + }) + .build({ availableUtxos: [...freshUtxos] }) + + const tx = await signBuilder.toTransaction() + + // Verify both requiredSigners are set + expect(tx.body.requiredSigners).toBeDefined() + expect(tx.body.requiredSigners?.length).toBe(2) + + const requiredHashes = tx.body.requiredSigners!.map((k) => KeyHash.toHex(k)) + expect(requiredHashes).toContain(KeyHash.toHex(credential1)) + expect(requiredHashes).toContain(KeyHash.toHex(credential2)) + + // Client1 creates a partial signature + const witness1 = await signBuilder.partialSign() + expect(witness1.vkeyWitnesses?.length).toBe(1) + + // Client2 signs the SAME transaction (not rebuilding it) + // Use the client's signTx method directly with the transaction object + const witness2 = await client2.signTx(tx) + expect(witness2.vkeyWitnesses?.length).toBe(1) + + // Assemble both witnesses into the final transaction + const submitBuilder = await signBuilder.assemble([witness1, witness2]) + + // Submit and verify confirmation + 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.Chain.test.ts b/packages/evolution-devnet/test/TxBuilder.Chain.test.ts new file mode 100644 index 00000000..1eff07eb --- /dev/null +++ b/packages/evolution-devnet/test/TxBuilder.Chain.test.ts @@ -0,0 +1,115 @@ +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 type { SignBuilder } from "@evolution-sdk/evolution/sdk/builders/SignBuilder" +import { createClient } from "@evolution-sdk/evolution/sdk/client/ClientImpl" + +describe("TxBuilder.chainResult", () => { + 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:1449", + ogmiosUrl: "http://localhost:1344" + }, + 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: "chain-test", + ports: { node: 6008, submit: 9009 }, + shelleyGenesis: genesisConfig, + kupo: { enabled: true, port: 1449, logLevel: "Info" }, + ogmios: { enabled: true, port: 1344, logLevel: "info" } + }) + + await Cluster.start(devnetCluster) + await new Promise((resolve) => setTimeout(resolve, 5_000)) + }, 180_000) + + afterAll(async () => { + if (devnetCluster) { + await Cluster.stop(devnetCluster) + await Cluster.remove(devnetCluster) + } + }, 60_000) + + it("should chain multiple transactions and submit them all", { timeout: 90_000 }, async () => { + const client = createTestClient(0) + const address = await client.address() + const TX_COUNT = 5 + + // Build chained transactions using build() + chainResult + let available = [...genesisUtxos] + const txs: Array = [] + + for (let i = 0; i < TX_COUNT; i++) { + const tx = await client + .newTx() + .payToAddress({ address, assets: Core.Assets.fromLovelace(10_000_000n) }) + .build({ availableUtxos: available }) + txs.push(tx) + available = [...tx.chainResult().available] + } + + // Verify all txHashes are unique + const txHashes = txs.map((tx) => tx.chainResult().txHash) + expect(new Set(txHashes).size).toBe(TX_COUNT) + + // Submit all transactions + const submittedHashes: Array = [] + for (const tx of txs) { + const hash = await tx.signAndSubmit() + submittedHashes.push(hash) + } + + // Verify computed hashes match submitted hashes + for (let i = 0; i < TX_COUNT; i++) { + expect(submittedHashes[i]).toBe(txs[i].chainResult().txHash) + } + + // Wait for all to confirm + for (const hash of submittedHashes) { + expect(await client.awaitTx(hash, 1000)).toBe(true) + } + }) +}) diff --git a/packages/evolution-devnet/test/TxBuilder.NativeScript.test.ts b/packages/evolution-devnet/test/TxBuilder.NativeScript.test.ts new file mode 100644 index 00000000..b65e5a33 --- /dev/null +++ b/packages/evolution-devnet/test/TxBuilder.NativeScript.test.ts @@ -0,0 +1,660 @@ +/** + * Devnet tests for TxBuilder native script operations. + * + * Tests native script functionality including: + * - Minting with native scripts + * - Spending from native script addresses + * - Multi-sig native scripts + */ + +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 * as NativeScripts from "@evolution-sdk/evolution/core/NativeScripts" +import * as ScriptHash from "@evolution-sdk/evolution/core/ScriptHash" +import * as Text from "@evolution-sdk/evolution/core/Text" +import * as UTxO from "@evolution-sdk/evolution/core/UTxO" +import { createClient } from "@evolution-sdk/evolution/sdk/client/ClientImpl" + +// Time utility functions (duplicated from core since Time module is not externally accessible) +const now = (): bigint => BigInt(Date.now()) +const unixTimeToSlot = (unixTime: bigint, slotConfig: Cluster.SlotConfig): bigint => { + const timePassed = unixTime - slotConfig.zeroTime + const slotsPassed = timePassed / BigInt(slotConfig.slotLength) + return slotsPassed + slotConfig.zeroSlot +} +const slotToUnixTime = (slot: bigint, slotConfig: Cluster.SlotConfig): bigint => { + const msAfterBegin = (slot - slotConfig.zeroSlot) * BigInt(slotConfig.slotLength) + return slotConfig.zeroTime + msAfterBegin +} + +describe("TxBuilder NativeScript (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:1449", + ogmiosUrl: "http://localhost:1344" + }, + 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: "nativescript-test", + ports: { node: 6007, submit: 9008 }, + shelleyGenesis: genesisConfig, + kupo: { enabled: true, port: 1449, logLevel: "Info" }, + ogmios: { enabled: true, port: 1344, logLevel: "info" } + }) + + await Cluster.start(devnetCluster) + await new Promise((resolve) => setTimeout(resolve, 5_000)) + }, 180_000) + + afterAll(async () => { + if (devnetCluster) { + await Cluster.stop(devnetCluster) + await Cluster.remove(devnetCluster) + } + }, 60_000) + + it("should handle multi-sig native script (all)", { timeout: 60_000 }, async () => { + const client1 = createTestClient(0) + const client2 = createTestClient(1) + + const address1 = await client1.address() + const address2 = await client2.address() + + const credential1 = address1.paymentCredential + const credential2 = address2.paymentCredential + + if (credential1._tag !== "KeyHash" || credential2._tag !== "KeyHash") { + throw new Error("Expected KeyHash credentials") + } + + // Create multi-sig script requiring BOTH signatures + const script1 = NativeScripts.makeScriptPubKey(credential1.hash) + const script2 = NativeScripts.makeScriptPubKey(credential2.hash) + const multiSigScript = NativeScripts.makeScriptAll([script1.script, script2.script]) + + const scriptHash = ScriptHash.fromScript(multiSigScript) + const policyId = ScriptHash.toHex(scriptHash) + const assetNameHex = Text.toHex("MultiSigToken") + const unit = policyId + assetNameHex + + // Build transaction with multi-sig mint + const signBuilder = await client1 + .newTx() + .attachScript({ script: multiSigScript }) + .mintAssets({ + assets: Core.Assets.fromRecord({ [unit]: 500n }) + }) + .payToAddress({ + address: address1, + assets: Core.Assets.fromLovelace(2_000_000n) + }) + .build({ availableUtxos: [...genesisUtxos] }) + + const tx = await signBuilder.toTransaction() + + // Verify mint and script + expect(tx.body.mint).toBeDefined() + expect(tx.witnessSet.nativeScripts).toBeDefined() + + // Client1 partial signs + const witness1 = await signBuilder.partialSign() + expect(witness1.vkeyWitnesses?.length).toBe(1) + + // Client2 signs the same transaction + const witness2 = await client2.signTx(tx) + expect(witness2.vkeyWitnesses?.length).toBe(1) + + // Assemble and submit + const submitBuilder = await signBuilder.assemble([witness1, witness2]) + const txHash = await submitBuilder.submit() + + expect(txHash.length).toBe(64) + + const confirmed = await client1.awaitTx(txHash, 1000) + expect(confirmed).toBe(true) + }) + + it("should handle multi-sig native script (any - 1 of N)", { timeout: 60_000 }, async () => { + const client1 = createTestClient(0) + const client2 = createTestClient(1) + + const address1 = await client1.address() + const address2 = await client2.address() + + const credential1 = address1.paymentCredential + const credential2 = address2.paymentCredential + + if (credential1._tag !== "KeyHash" || credential2._tag !== "KeyHash") { + throw new Error("Expected KeyHash credentials") + } + + // Create multi-sig script requiring ANY ONE signature (1-of-2) + const script1 = NativeScripts.makeScriptPubKey(credential1.hash) + const script2 = NativeScripts.makeScriptPubKey(credential2.hash) + const anyScript = NativeScripts.makeScriptAny([script1.script, script2.script]) + + const scriptHash = ScriptHash.fromScript(anyScript) + const policyId = ScriptHash.toHex(scriptHash) + const assetNameHex = Text.toHex("AnyOneToken") + const unit = policyId + assetNameHex + + // Build transaction - only client1 needs to sign + // Client fetches UTxOs automatically from the provider + const signBuilder = await client1 + .newTx() + .attachScript({ script: anyScript }) + .mintAssets({ + assets: Core.Assets.fromRecord({ [unit]: 100n }) + }) + .payToAddress({ + address: address1, + assets: Core.Assets.fromLovelace(2_000_000n) + }) + .build() + + const tx = await signBuilder.toTransaction() + + expect(tx.body.mint).toBeDefined() + expect(tx.witnessSet.nativeScripts).toBeDefined() + + // Only client1 signs - should be sufficient for "any" + 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 handle N-of-K native script (2 of 3)", { timeout: 60_000 }, async () => { + const client1 = createTestClient(0) + const client2 = createTestClient(1) + const client3 = createTestClient(2) + + const address1 = await client1.address() + const address2 = await client2.address() + const address3 = await client3.address() + + const credential1 = address1.paymentCredential + const credential2 = address2.paymentCredential + const credential3 = address3.paymentCredential + + if (credential1._tag !== "KeyHash" || credential2._tag !== "KeyHash" || credential3._tag !== "KeyHash") { + throw new Error("Expected KeyHash credentials") + } + + // Create 2-of-3 multi-sig script + const script1 = NativeScripts.makeScriptPubKey(credential1.hash) + const script2 = NativeScripts.makeScriptPubKey(credential2.hash) + const script3 = NativeScripts.makeScriptPubKey(credential3.hash) + const nOfKScript = NativeScripts.makeScriptNOfK(2n, [script1.script, script2.script, script3.script]) + + const scriptHash = ScriptHash.fromScript(nOfKScript) + const policyId = ScriptHash.toHex(scriptHash) + const assetNameHex = Text.toHex("TwoOfThreeToken") + const unit = policyId + assetNameHex + + // Build transaction - client fetches UTxOs automatically + const signBuilder = await client1 + .newTx() + .attachScript({ script: nOfKScript }) + .mintAssets({ + assets: Core.Assets.fromRecord({ [unit]: 200n }) + }) + .payToAddress({ + address: address1, + assets: Core.Assets.fromLovelace(2_000_000n) + }) + .build() + + const tx = await signBuilder.toTransaction() + + expect(tx.body.mint).toBeDefined() + expect(tx.witnessSet.nativeScripts).toBeDefined() + + // Client1 and Client2 sign (2 of 3 - Client3 not needed) + const witness1 = await signBuilder.partialSign() + const witness2 = await client2.signTx(tx) + + // Assemble with only 2 signatures + const submitBuilder = await signBuilder.assemble([witness1, witness2]) + const txHash = await submitBuilder.submit() + + expect(txHash.length).toBe(64) + + const confirmed = await client1.awaitTx(txHash, 1000) + expect(confirmed).toBe(true) + }) + + it("should handle time-locked native script (invalidHereafter)", { timeout: 60_000 }, async () => { + if (!devnetCluster) throw new Error("Cluster not initialized") + + const client = createTestClient(0) + const myAddress = await client.address() + + const paymentCredential = myAddress.paymentCredential + if (paymentCredential._tag !== "KeyHash") { + throw new Error("Expected KeyHash credential") + } + + // Use the same slot config that the client uses + const slotConfig = Cluster.getSlotConfig(devnetCluster) + const currentTime = now() + const currentSlot = unixTimeToSlot(currentTime, slotConfig) + + // Set future slot 1000 slots ahead (20 seconds at 0.02s/slot) + const futureSlot = currentSlot + 1000n + const futureUnixTime = slotToUnixTime(futureSlot, slotConfig) + + // Create time-locked script: signature required AND must be before futureSlot + const sigScript = NativeScripts.makeScriptPubKey(paymentCredential.hash) + const timeScript = NativeScripts.makeInvalidHereafter(futureSlot) + const timelockScript = NativeScripts.makeScriptAll([sigScript.script, timeScript.script]) + + const scriptHash = ScriptHash.fromScript(timelockScript) + const policyId = ScriptHash.toHex(scriptHash) + const assetNameHex = Text.toHex("TimeLockToken") + const unit = policyId + assetNameHex + + // Build transaction - setValidity uses the same slot config via the client + const signBuilder = await client + .newTx() + .attachScript({ script: timelockScript }) + .setValidity({ to: futureUnixTime }) + .mintAssets({ + assets: Core.Assets.fromRecord({ [unit]: 50n }) + }) + .payToAddress({ + address: myAddress, + assets: Core.Assets.fromLovelace(2_000_000n) + }) + .build() + + const tx = await signBuilder.toTransaction() + + // Verify time constraints are set correctly + expect(tx.body.mint).toBeDefined() + expect(tx.body.ttl).toBeDefined() + expect(tx.body.ttl).toBe(futureSlot) + + 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 handle complex nested native script (sig AND (any of time conditions))", { timeout: 60_000 }, async () => { + if (!devnetCluster) throw new Error("Cluster not initialized") + + const client = createTestClient(0) + const myAddress = await client.address() + + const paymentCredential = myAddress.paymentCredential + if (paymentCredential._tag !== "KeyHash") { + throw new Error("Expected KeyHash credential") + } + + // Use the same slot config that the client uses + const slotConfig = Cluster.getSlotConfig(devnetCluster) + const currentTime = now() + const currentSlot = unixTimeToSlot(currentTime, slotConfig) + + // beforeSlot: 2000 slots ahead (~40 seconds) - InvalidHereafter will be satisfied + // afterSlot: 10000 slots ahead (~200 seconds) - InvalidBefore will NOT be satisfied yet + const beforeSlot = currentSlot + 2000n + const afterSlot = currentSlot + 10000n + const beforeUnixTime = slotToUnixTime(beforeSlot, slotConfig) + + // Complex script structure: + // ALL of: + // - Signature from our key + // - ANY of: + // - Before slot (current + 2000) <- this one will be satisfied + // - After slot (current + 10000) <- this one won't be satisfied yet + const sigScript = NativeScripts.makeScriptPubKey(paymentCredential.hash) + const beforeSlotScript = NativeScripts.makeInvalidHereafter(beforeSlot) + const afterSlotScript = NativeScripts.makeInvalidBefore(afterSlot) + + const timeOptionsScript = NativeScripts.makeScriptAny([beforeSlotScript.script, afterSlotScript.script]) + const complexScript = NativeScripts.makeScriptAll([sigScript.script, timeOptionsScript.script]) + + const scriptHash = ScriptHash.fromScript(complexScript) + const policyId = ScriptHash.toHex(scriptHash) + const assetNameHex = Text.toHex("ComplexToken") + const unit = policyId + assetNameHex + + // Use the "before slot" option by setting TTL to beforeSlot + const signBuilder = await client + .newTx() + .attachScript({ script: complexScript }) + .setValidity({ to: beforeUnixTime }) + .mintAssets({ + assets: Core.Assets.fromRecord({ [unit]: 25n }) + }) + .payToAddress({ + address: myAddress, + assets: Core.Assets.fromLovelace(2_000_000n) + }) + .build() + + const tx = await signBuilder.toTransaction() + + expect(tx.body.mint).toBeDefined() + expect(tx.body.ttl).toBe(beforeSlot) + expect(tx.witnessSet.nativeScripts).toBeDefined() + + 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 spend from a 2-of-2 multi-sig script address", { timeout: 60_000 }, async () => { + const client1 = createTestClient(0) + const client2 = createTestClient(1) + + const address1 = await client1.address() + const address2 = await client2.address() + + const credential1 = address1.paymentCredential + const credential2 = address2.paymentCredential + + if (credential1._tag !== "KeyHash" || credential2._tag !== "KeyHash") { + throw new Error("Expected KeyHash credentials") + } + + // Create 2-of-2 multi-sig script + const script1 = NativeScripts.makeScriptPubKey(credential1.hash) + const script2 = NativeScripts.makeScriptPubKey(credential2.hash) + const multiSigScript = NativeScripts.makeScriptAll([script1.script, script2.script]) + + const scriptHash = ScriptHash.fromScript(multiSigScript) + + // Create script address + const scriptAddress = new Address.Address({ + networkId: 0, + paymentCredential: scriptHash, + stakingCredential: undefined + }) + + // Fund the script address - client fetches UTxOs automatically + const fundSignBuilder = await client1 + .newTx() + .payToAddress({ + address: scriptAddress, + assets: Core.Assets.fromLovelace(10_000_000n) + }) + .build() + + const fundSubmitBuilder = await fundSignBuilder.sign() + const fundTxHash = await fundSubmitBuilder.submit() + + await client1.awaitTx(fundTxHash, 1000) + await new Promise((resolve) => setTimeout(resolve, 2_000)) + + // Fetch UTxOs at the script address + const scriptUtxos = await client1.getUtxos(scriptAddress) + expect(scriptUtxos.length).toBeGreaterThan(0) + + const scriptUtxo = scriptUtxos.find( + (u) => UTxO.toOutRefString(u).startsWith(fundTxHash) + ) + expect(scriptUtxo).toBeDefined() + + // Spend from multi-sig script - requires both signatures + // Client fetches wallet UTxOs automatically, but we explicitly add the script UTxO + const spendSignBuilder = await client1 + .newTx() + .attachScript({ script: multiSigScript }) + .collectFrom({ inputs: [scriptUtxo!] }) + .payToAddress({ + address: address1, + assets: Core.Assets.fromLovelace(5_000_000n) + }) + .build() + + const spendTx = await spendSignBuilder.toTransaction() + + // Both clients must sign + const witness1 = await spendSignBuilder.partialSign() + const witness2 = await client2.signTx(spendTx) + + const spendSubmitBuilder = await spendSignBuilder.assemble([witness1, witness2]) + const spendTxHash = await spendSubmitBuilder.submit() + + expect(spendTxHash.length).toBe(64) + + const spendConfirmed = await client1.awaitTx(spendTxHash, 1000) + expect(spendConfirmed).toBe(true) + }) + + it("should use native script as reference script for minting", { timeout: 60_000 }, async () => { + const client1 = createTestClient(0) + const client2 = createTestClient(1) + + const address1 = await client1.address() + const address2 = await client2.address() + + const credential1 = address1.paymentCredential + const credential2 = address2.paymentCredential + + if (credential1._tag !== "KeyHash" || credential2._tag !== "KeyHash") { + throw new Error("Expected KeyHash credentials") + } + + // Create a 2-of-2 multi-sig native script + const script1 = NativeScripts.makeScriptPubKey(credential1.hash) + const script2 = NativeScripts.makeScriptPubKey(credential2.hash) + const multiSigScript = NativeScripts.makeScriptAll([script1.script, script2.script]) + + const scriptHash = ScriptHash.fromScript(multiSigScript) + const policyId = ScriptHash.toHex(scriptHash) + + // Step 1: Create a UTxO with the native script as a reference script + const refScriptSignBuilder = await client1 + .newTx() + .payToAddress({ + address: address1, + assets: Core.Assets.fromLovelace(5_000_000n), + script: multiSigScript + }) + .build() + + const refScriptSubmitBuilder = await refScriptSignBuilder.sign() + const refScriptTxHash = await refScriptSubmitBuilder.submit() + + expect(refScriptTxHash.length).toBe(64) + await client1.awaitTx(refScriptTxHash, 1000) + await new Promise((resolve) => setTimeout(resolve, 2_000)) + + // Find the UTxO with the reference script + const walletUtxos = await client1.getUtxos(address1) + const refScriptUtxo = walletUtxos.find( + (u) => UTxO.toOutRefString(u).startsWith(refScriptTxHash) && u.scriptRef !== undefined + ) + expect(refScriptUtxo).toBeDefined() + expect(refScriptUtxo!.scriptRef).toBeDefined() + + // Step 2: Mint using the reference script (readFrom) instead of attachScript + const assetNameHex = Text.toHex("RefMinted") + const unit = policyId + assetNameHex + + const mintSignBuilder = await client1 + .newTx() + .readFrom({ referenceInputs: [refScriptUtxo!] }) // Reference the UTxO with the script + .mintAssets({ + assets: Core.Assets.fromRecord({ [unit]: 100n }) + }) + .payToAddress({ + address: address1, + assets: Core.Assets.fromLovelace(2_000_000n) + }) + .build() + + const mintTx = await mintSignBuilder.toTransaction() + + // Both signers still need to sign (native script requires signatures) + // When using client.signTx directly, reference UTxOs are auto-fetched + // so the wallet can determine required signers from reference scripts + const mintWitness1 = await mintSignBuilder.partialSign() + const mintWitness2 = await client2.signTx(mintTx) + + const mintSubmitBuilder = await mintSignBuilder.assemble([mintWitness1, mintWitness2]) + const mintTxHash = await mintSubmitBuilder.submit() + + expect(mintTxHash.length).toBe(64) + + const mintConfirmed = await client1.awaitTx(mintTxHash, 1000) + expect(mintConfirmed).toBe(true) + }) + + it("should spend from script address using native script as reference input", { timeout: 60_000 }, async () => { + const client1 = createTestClient(0) + const client2 = createTestClient(1) + + const address1 = await client1.address() + const address2 = await client2.address() + + const credential1 = address1.paymentCredential + const credential2 = address2.paymentCredential + + if (credential1._tag !== "KeyHash" || credential2._tag !== "KeyHash") { + throw new Error("Expected KeyHash credentials") + } + + // Create a 2-of-2 multi-sig native script + const script1 = NativeScripts.makeScriptPubKey(credential1.hash) + const script2 = NativeScripts.makeScriptPubKey(credential2.hash) + const multiSigScript = NativeScripts.makeScriptAll([script1.script, script2.script]) + + const scriptHash = ScriptHash.fromScript(multiSigScript) + + // Create script address + const scriptAddress = new Address.Address({ + networkId: 0, + paymentCredential: scriptHash, + stakingCredential: undefined + }) + + // Step 1: Create a UTxO with the native script as a reference script + const refScriptSignBuilder = await client1 + .newTx() + .payToAddress({ + address: address1, + assets: Core.Assets.fromLovelace(5_000_000n), + script: multiSigScript + }) + .build() + + const refScriptSubmitBuilder = await refScriptSignBuilder.sign() + const refScriptTxHash = await refScriptSubmitBuilder.submit() + + expect(refScriptTxHash.length).toBe(64) + await client1.awaitTx(refScriptTxHash, 1000) + await new Promise((resolve) => setTimeout(resolve, 2_000)) + + // Step 2: Fund the script address + const fundSignBuilder = await client1 + .newTx() + .payToAddress({ + address: scriptAddress, + assets: Core.Assets.fromLovelace(10_000_000n) + }) + .build() + + const fundSubmitBuilder = await fundSignBuilder.sign() + const fundTxHash = await fundSubmitBuilder.submit() + + await client1.awaitTx(fundTxHash, 1000) + await new Promise((resolve) => setTimeout(resolve, 2_000)) + + // Fetch UTxOs at the script address + const scriptUtxos = await client1.getUtxos(scriptAddress) + expect(scriptUtxos.length).toBeGreaterThan(0) + + const scriptUtxo = scriptUtxos.find((u) => UTxO.toOutRefString(u).startsWith(fundTxHash)) + expect(scriptUtxo).toBeDefined() + + // Find the UTxO with the reference script (fetch AFTER fund tx to get fresh state) + const walletUtxos = await client1.getUtxos(address1) + const refScriptUtxo = walletUtxos.find( + (u) => UTxO.toOutRefString(u).startsWith(refScriptTxHash) && u.scriptRef !== undefined + ) + expect(refScriptUtxo).toBeDefined() + expect(refScriptUtxo!.scriptRef).toBeDefined() + + // Step 3: Spend from script address using readFrom (reference input) instead of attachScript + // This tests that native scripts provided via reference inputs don't incorrectly require redeemers + const spendSignBuilder = await client1 + .newTx() + .readFrom({ referenceInputs: [refScriptUtxo!] }) // Reference the UTxO with the script + .collectFrom({ inputs: [scriptUtxo!] }) // Spend from the script address + .payToAddress({ + address: address1, + assets: Core.Assets.fromLovelace(5_000_000n) + }) + .build() + + const spendTx = await spendSignBuilder.toTransaction() + + // Both clients must sign (native script requires signatures) + const witness1 = await spendSignBuilder.partialSign() + const witness2 = await client2.signTx(spendTx) + + const spendSubmitBuilder = await spendSignBuilder.assemble([witness1, witness2]) + const spendTxHash = await spendSubmitBuilder.submit() + + expect(spendTxHash.length).toBe(64) + const spendConfirmed = await client1.awaitTx(spendTxHash, 1000) + expect(spendConfirmed).toBe(true) + }) +}) diff --git a/packages/evolution-devnet/test/utils/utxo-helpers.ts b/packages/evolution-devnet/test/utils/utxo-helpers.ts index d102fd1d..28f5e226 100644 --- a/packages/evolution-devnet/test/utils/utxo-helpers.ts +++ b/packages/evolution-devnet/test/utils/utxo-helpers.ts @@ -2,8 +2,7 @@ import { Core } from "@evolution-sdk/evolution" import * as CoreAddress from "@evolution-sdk/evolution/core/Address" import * as CoreData from "@evolution-sdk/evolution/core/Data" import * as CoreDatumOption from "@evolution-sdk/evolution/core/DatumOption" -import * as CoreScript from "@evolution-sdk/evolution/core/Script" -import * as CoreScriptRef from "@evolution-sdk/evolution/core/ScriptRef" +import type * as CoreScript from "@evolution-sdk/evolution/core/Script" import * as CoreTransactionHash from "@evolution-sdk/evolution/core/TransactionHash" import * as CoreUTxO from "@evolution-sdk/evolution/core/UTxO" import type * as Datum from "@evolution-sdk/evolution/sdk/Datum" @@ -103,20 +102,12 @@ export const createCoreTestUtxo = (options: CreateCoreTestUtxoOptions): CoreUTxO } } - // Convert Core Script to ScriptRef - let coreScriptRef: CoreScriptRef.ScriptRef | undefined - if (scriptRef) { - // Convert Script to ScriptRef bytes (CBOR-encoded script) - const scriptBytes = CoreScript.toCBOR(scriptRef) - coreScriptRef = new CoreScriptRef.ScriptRef({ bytes: scriptBytes }) - } - return new CoreUTxO.UTxO({ transactionId: CoreTransactionHash.fromHex(paddedTxId), index: BigInt(index), address: CoreAddress.fromBech32(address), assets, - scriptRef: coreScriptRef, + scriptRef, datumOption: coreDatumOption }) } diff --git a/packages/evolution/docs/modules/core/NativeScripts.ts.md b/packages/evolution/docs/modules/core/NativeScripts.ts.md index ecb532ca..1fd0a968 100644 --- a/packages/evolution/docs/modules/core/NativeScripts.ts.md +++ b/packages/evolution/docs/modules/core/NativeScripts.ts.md @@ -43,6 +43,7 @@ parent: Modules - [arbitrary](#arbitrary) - [utilities](#utilities) - [countRequiredSigners](#countrequiredsigners) + - [extractKeyHashes](#extractkeyhashes) - [utils](#utils) - [CDDLSchema](#cddlschema) - [FromCBORBytes](#fromcborbytes-1) @@ -374,6 +375,19 @@ export declare const countRequiredSigners: (script: NativeScriptVariants) => num Added in v2.0.0 +## extractKeyHashes + +Extract all key hashes from a native script. +Recursively traverses nested scripts to find all ScriptPubKey key hashes. + +**Signature** + +```ts +export declare const extractKeyHashes: (script: NativeScriptVariants) => ReadonlyArray +``` + +Added in v2.0.0 + # utils ## CDDLSchema diff --git a/packages/evolution/docs/modules/core/UTxO.ts.md b/packages/evolution/docs/modules/core/UTxO.ts.md index 8cffbfa7..f768529c 100644 --- a/packages/evolution/docs/modules/core/UTxO.ts.md +++ b/packages/evolution/docs/modules/core/UTxO.ts.md @@ -25,8 +25,8 @@ parent: Modules - [datumOptionToSDK](#datumoptiontosdk) - [fromSDK](#fromsdk) - [fromSDKArray](#fromsdkarray) - - [scriptRefFromSDK](#scriptreffromsdk) - - [scriptRefToSDKHex](#scriptreftosdkhex) + - [scriptFromSDK](#scriptfromsdk) + - [scriptToSDK](#scripttosdk) - [toArray](#toarray) - [toSDK](#tosdk) - [toSDKArray](#tosdkarray) @@ -37,7 +37,6 @@ parent: Modules - [UTxO (class)](#utxo-class) - [toJSON (method)](#tojson-method) - [toString (method)](#tostring-method) - - [[Inspectable.NodeInspectSymbol] (method)](#inspectablenodeinspectsymbol-method) - [[Equal.symbol] (method)](#equalsymbol-method) - [[Hash.symbol] (method)](#hashsymbol-method) - [models](#models) @@ -207,29 +206,28 @@ export declare const fromSDKArray: ( Added in v2.0.0 -## scriptRefFromSDK +## scriptFromSDK -Convert SDK Script to Core ScriptRef. +Convert SDK Script to Core Script. **Signature** ```ts -export declare const scriptRefFromSDK: ( - script: SDKScript.Script -) => Effect.Effect +export declare const scriptFromSDK: ( + sdkScript: SDKScript.Script +) => Effect.Effect ``` Added in v2.0.0 -## scriptRefToSDKHex +## scriptToSDK -Convert Core ScriptRef to SDK Script type string. -Note: We lose the script type information as ScriptRef only stores bytes. +Convert Core Script to SDK Script. **Signature** ```ts -export declare const scriptRefToSDKHex: (scriptRef: ScriptRef.ScriptRef) => string +export declare const scriptToSDK: (script: Script.Script) => SDKScript.Script ``` Added in v2.0.0 @@ -332,14 +330,6 @@ toJSON() toString(): string ``` -### [Inspectable.NodeInspectSymbol] (method) - -**Signature** - -```ts -[Inspectable.NodeInspectSymbol](): unknown -``` - ### [Equal.symbol] (method) **Signature** diff --git a/packages/evolution/docs/modules/sdk/Credential.ts.md b/packages/evolution/docs/modules/sdk/Credential.ts.md index 892e0e40..69141c8b 100644 --- a/packages/evolution/docs/modules/sdk/Credential.ts.md +++ b/packages/evolution/docs/modules/sdk/Credential.ts.md @@ -1,6 +1,6 @@ --- title: sdk/Credential.ts -nav_order: 180 +nav_order: 181 parent: Modules --- diff --git a/packages/evolution/docs/modules/sdk/Datum.ts.md b/packages/evolution/docs/modules/sdk/Datum.ts.md index fbcdf898..b4841618 100644 --- a/packages/evolution/docs/modules/sdk/Datum.ts.md +++ b/packages/evolution/docs/modules/sdk/Datum.ts.md @@ -1,6 +1,6 @@ --- title: sdk/Datum.ts -nav_order: 181 +nav_order: 182 parent: Modules --- diff --git a/packages/evolution/docs/modules/sdk/Delegation.ts.md b/packages/evolution/docs/modules/sdk/Delegation.ts.md index e6dab3da..d724b53e 100644 --- a/packages/evolution/docs/modules/sdk/Delegation.ts.md +++ b/packages/evolution/docs/modules/sdk/Delegation.ts.md @@ -1,6 +1,6 @@ --- title: sdk/Delegation.ts -nav_order: 182 +nav_order: 183 parent: Modules --- diff --git a/packages/evolution/docs/modules/sdk/EvalRedeemer.ts.md b/packages/evolution/docs/modules/sdk/EvalRedeemer.ts.md index bcdffad2..014344a1 100644 --- a/packages/evolution/docs/modules/sdk/EvalRedeemer.ts.md +++ b/packages/evolution/docs/modules/sdk/EvalRedeemer.ts.md @@ -1,6 +1,6 @@ --- title: sdk/EvalRedeemer.ts -nav_order: 183 +nav_order: 184 parent: Modules --- diff --git a/packages/evolution/docs/modules/sdk/Network.ts.md b/packages/evolution/docs/modules/sdk/Network.ts.md index f6cd8772..5c926d3a 100644 --- a/packages/evolution/docs/modules/sdk/Network.ts.md +++ b/packages/evolution/docs/modules/sdk/Network.ts.md @@ -1,6 +1,6 @@ --- title: sdk/Network.ts -nav_order: 184 +nav_order: 185 parent: Modules --- diff --git a/packages/evolution/docs/modules/sdk/OutRef.ts.md b/packages/evolution/docs/modules/sdk/OutRef.ts.md index f24dec3a..fe9f8f83 100644 --- a/packages/evolution/docs/modules/sdk/OutRef.ts.md +++ b/packages/evolution/docs/modules/sdk/OutRef.ts.md @@ -1,6 +1,6 @@ --- title: sdk/OutRef.ts -nav_order: 185 +nav_order: 186 parent: Modules --- diff --git a/packages/evolution/docs/modules/sdk/PolicyId.ts.md b/packages/evolution/docs/modules/sdk/PolicyId.ts.md index 3df237fc..c96372ff 100644 --- a/packages/evolution/docs/modules/sdk/PolicyId.ts.md +++ b/packages/evolution/docs/modules/sdk/PolicyId.ts.md @@ -1,6 +1,6 @@ --- title: sdk/PolicyId.ts -nav_order: 186 +nav_order: 187 parent: Modules --- diff --git a/packages/evolution/docs/modules/sdk/PoolParams.ts.md b/packages/evolution/docs/modules/sdk/PoolParams.ts.md index 297963d5..326f468c 100644 --- a/packages/evolution/docs/modules/sdk/PoolParams.ts.md +++ b/packages/evolution/docs/modules/sdk/PoolParams.ts.md @@ -1,6 +1,6 @@ --- title: sdk/PoolParams.ts -nav_order: 187 +nav_order: 188 parent: Modules --- diff --git a/packages/evolution/docs/modules/sdk/ProtocolParameters.ts.md b/packages/evolution/docs/modules/sdk/ProtocolParameters.ts.md index c46648d1..d2de97e0 100644 --- a/packages/evolution/docs/modules/sdk/ProtocolParameters.ts.md +++ b/packages/evolution/docs/modules/sdk/ProtocolParameters.ts.md @@ -1,6 +1,6 @@ --- title: sdk/ProtocolParameters.ts -nav_order: 188 +nav_order: 189 parent: Modules --- diff --git a/packages/evolution/docs/modules/sdk/Relay.ts.md b/packages/evolution/docs/modules/sdk/Relay.ts.md index b3f02220..02da4cc1 100644 --- a/packages/evolution/docs/modules/sdk/Relay.ts.md +++ b/packages/evolution/docs/modules/sdk/Relay.ts.md @@ -1,6 +1,6 @@ --- title: sdk/Relay.ts -nav_order: 194 +nav_order: 195 parent: Modules --- diff --git a/packages/evolution/docs/modules/sdk/RewardAddress.ts.md b/packages/evolution/docs/modules/sdk/RewardAddress.ts.md index f6c8c04b..96e88f15 100644 --- a/packages/evolution/docs/modules/sdk/RewardAddress.ts.md +++ b/packages/evolution/docs/modules/sdk/RewardAddress.ts.md @@ -1,6 +1,6 @@ --- title: sdk/RewardAddress.ts -nav_order: 195 +nav_order: 196 parent: Modules --- diff --git a/packages/evolution/docs/modules/sdk/Script.ts.md b/packages/evolution/docs/modules/sdk/Script.ts.md index a61db0ae..26eb5a29 100644 --- a/packages/evolution/docs/modules/sdk/Script.ts.md +++ b/packages/evolution/docs/modules/sdk/Script.ts.md @@ -1,6 +1,6 @@ --- title: sdk/Script.ts -nav_order: 196 +nav_order: 197 parent: Modules --- diff --git a/packages/evolution/docs/modules/sdk/Type.ts.md b/packages/evolution/docs/modules/sdk/Type.ts.md index 57134ead..db24d1d3 100644 --- a/packages/evolution/docs/modules/sdk/Type.ts.md +++ b/packages/evolution/docs/modules/sdk/Type.ts.md @@ -1,6 +1,6 @@ --- title: sdk/Type.ts -nav_order: 197 +nav_order: 198 parent: Modules --- diff --git a/packages/evolution/docs/modules/sdk/UTxO.ts.md b/packages/evolution/docs/modules/sdk/UTxO.ts.md index ce74c4a4..768783be 100644 --- a/packages/evolution/docs/modules/sdk/UTxO.ts.md +++ b/packages/evolution/docs/modules/sdk/UTxO.ts.md @@ -1,6 +1,6 @@ --- title: sdk/UTxO.ts -nav_order: 199 +nav_order: 200 parent: Modules --- diff --git a/packages/evolution/docs/modules/sdk/Unit.ts.md b/packages/evolution/docs/modules/sdk/Unit.ts.md index 4d1570b8..47c3241e 100644 --- a/packages/evolution/docs/modules/sdk/Unit.ts.md +++ b/packages/evolution/docs/modules/sdk/Unit.ts.md @@ -1,6 +1,6 @@ --- title: sdk/Unit.ts -nav_order: 198 +nav_order: 199 parent: Modules --- diff --git a/packages/evolution/docs/modules/sdk/builders/RedeemerBuilder.ts.md b/packages/evolution/docs/modules/sdk/builders/RedeemerBuilder.ts.md index 2c3228b5..b252d460 100644 --- a/packages/evolution/docs/modules/sdk/builders/RedeemerBuilder.ts.md +++ b/packages/evolution/docs/modules/sdk/builders/RedeemerBuilder.ts.md @@ -1,6 +1,6 @@ --- title: sdk/builders/RedeemerBuilder.ts -nav_order: 169 +nav_order: 170 parent: Modules --- diff --git a/packages/evolution/docs/modules/sdk/builders/SignBuilder.ts.md b/packages/evolution/docs/modules/sdk/builders/SignBuilder.ts.md index a98b0caf..b327deb0 100644 --- a/packages/evolution/docs/modules/sdk/builders/SignBuilder.ts.md +++ b/packages/evolution/docs/modules/sdk/builders/SignBuilder.ts.md @@ -1,6 +1,6 @@ --- title: sdk/builders/SignBuilder.ts -nav_order: 170 +nav_order: 171 parent: Modules --- @@ -25,11 +25,21 @@ SignBuilder extends TransactionResultBase with signing capabilities. Only available when the client has a signing wallet (seed, private key, or API wallet). Provides access to unsigned transaction (via base interface) and signing operations. +Includes `chainResult` for transaction chaining - use `chainResult.available` as +`availableUtxos` for the next transaction in a chain. + **Signature** ```ts export interface SignBuilder extends TransactionResultBase, EffectToPromiseAPI { readonly Effect: SignBuilderEffect + /** + * Compute chain result for building dependent transactions. + * Contains consumed UTxOs, available UTxOs (remaining + created), and txHash. + * + * Result is memoized - computed once on first call, cached for subsequent calls. + */ + readonly chainResult: () => ChainResult } ``` @@ -52,6 +62,7 @@ export interface SignBuilderEffect { // Signing methods readonly sign: () => Effect.Effect + readonly signAndSubmit: () => Effect.Effect readonly signWithWitness: ( witnessSet: TransactionWitnessSet.TransactionWitnessSet ) => Effect.Effect diff --git a/packages/evolution/docs/modules/sdk/builders/SignBuilderImpl.ts.md b/packages/evolution/docs/modules/sdk/builders/SignBuilderImpl.ts.md index a4a69e78..17ba8dd2 100644 --- a/packages/evolution/docs/modules/sdk/builders/SignBuilderImpl.ts.md +++ b/packages/evolution/docs/modules/sdk/builders/SignBuilderImpl.ts.md @@ -1,6 +1,6 @@ --- title: sdk/builders/SignBuilderImpl.ts -nav_order: 171 +nav_order: 172 parent: Modules --- @@ -43,8 +43,11 @@ export declare const makeSignBuilder: (params: { transactionWithFakeWitnesses: Transaction.Transaction fee: bigint utxos: ReadonlyArray + referenceUtxos: ReadonlyArray provider: Provider.Provider wallet: Wallet + outputs: ReadonlyArray + availableUtxos: ReadonlyArray }) => SignBuilder ``` diff --git a/packages/evolution/docs/modules/sdk/builders/SubmitBuilder.ts.md b/packages/evolution/docs/modules/sdk/builders/SubmitBuilder.ts.md index 6fc395d8..62649e15 100644 --- a/packages/evolution/docs/modules/sdk/builders/SubmitBuilder.ts.md +++ b/packages/evolution/docs/modules/sdk/builders/SubmitBuilder.ts.md @@ -1,6 +1,6 @@ --- title: sdk/builders/SubmitBuilder.ts -nav_order: 172 +nav_order: 173 parent: Modules --- diff --git a/packages/evolution/docs/modules/sdk/builders/SubmitBuilderImpl.ts.md b/packages/evolution/docs/modules/sdk/builders/SubmitBuilderImpl.ts.md index 9c29a874..7f5b37f6 100644 --- a/packages/evolution/docs/modules/sdk/builders/SubmitBuilderImpl.ts.md +++ b/packages/evolution/docs/modules/sdk/builders/SubmitBuilderImpl.ts.md @@ -1,6 +1,6 @@ --- title: sdk/builders/SubmitBuilderImpl.ts -nav_order: 173 +nav_order: 174 parent: Modules --- diff --git a/packages/evolution/docs/modules/sdk/builders/TransactionBuilder.ts.md b/packages/evolution/docs/modules/sdk/builders/TransactionBuilder.ts.md index 8ce7655e..76898f97 100644 --- a/packages/evolution/docs/modules/sdk/builders/TransactionBuilder.ts.md +++ b/packages/evolution/docs/modules/sdk/builders/TransactionBuilder.ts.md @@ -1,6 +1,6 @@ --- title: sdk/builders/TransactionBuilder.ts -nav_order: 174 +nav_order: 175 parent: Modules --- @@ -594,6 +594,71 @@ export interface TransactionBuilderBase { * @category validity-methods */ readonly setValidity: (params: ValidityParams) => this + + /** + * Add a required signer to the transaction. + * + * Adds a key hash to the transaction's requiredSigners field. This is used to + * require specific key signatures even when those keys don't control inputs. + * Common use cases include: + * - Multi-sig schemes requiring explicit signature verification + * - Plutus scripts that check for specific signers in the transaction + * - Governance transactions requiring DRep or committee member signatures + * + * Duplicate key hashes are automatically deduplicated. + * + * Queues a deferred operation that will be executed when build() is called. + * Returns the same builder for method chaining. + * + * @example + * ```typescript + * import * as KeyHash from "@evolution-sdk/core/KeyHash" + * import * as Address from "@evolution-sdk/core/Address" + * + * // Add signer from address credential + * const address = Address.fromBech32("addr_test1...") + * const cred = address.paymentCredential + * if (cred._tag === "KeyHash") { + * const tx = await builder + * .addSigner({ keyHash: cred }) + * .build() + * } + * ``` + * + * @since 2.0.0 + * @category builder-methods + */ + readonly addSigner: (params: AddSignerParams) => this + + // ============================================================================ + // Transaction Chaining Methods + // ============================================================================ + + /** + * Execute transaction build and return consumed/available UTxOs for chaining. + * + * Runs the full build pipeline (coin selection, fee calculation, evaluation) and returns + * which UTxOs were consumed and which remain available for subsequent transactions. + * Use this when building multiple dependent transactions in sequence. + * + * @returns Promise with consumed and available UTxOs + * + * @example + * ```typescript + * // Build first transaction, get remaining UTxOs + * const tx1 = await builder + * .payTo({ address, value: { lovelace: 5_000_000n } }) + * .build({ availableUtxos: walletUtxos }) + * + * // Build second transaction using remaining UTxOs from chainResult + * const tx2 = await builder + * .payTo({ address, value: { lovelace: 3_000_000n } }) + * .build({ availableUtxos: tx1.chainResult().available }) + * ``` + * + * @since 2.0.0 + * @category chaining-methods + */ } ```` @@ -986,17 +1051,22 @@ Added in v2.0.0 Result type for transaction chaining operations. -**NOTE: NOT YET IMPLEMENTED** - This interface is reserved for future implementation -of multi-transaction workflows. Current chain methods return stub implementations. +Provides consumed and available UTxOs for building chained transactions. +The available UTxOs include both remaining unspent inputs AND newly created outputs +with pre-computed txHash, ready to be spent in subsequent transactions. + +Accessed via `SignBuilder.chainResult()` after calling `build()`. **Signature** ```ts export interface ChainResult { - readonly transaction: Transaction.Transaction - readonly newOutputs: ReadonlyArray // UTxOs created by this transaction - readonly updatedUtxos: ReadonlyArray // Available UTxOs for next transaction (original - spent + new) - readonly spentUtxos: ReadonlyArray // UTxOs consumed by this transaction + /** UTxOs consumed from availableUtxos by coin selection */ + readonly consumed: ReadonlyArray + /** Available UTxOs: remaining unspent + newly created (with computed txHash) */ + readonly available: ReadonlyArray + /** Pre-computed transaction hash (blake2b-256 of transaction body) */ + readonly txHash: string } ``` @@ -1134,6 +1204,7 @@ export interface TxBuilderState { readonly from?: Time.UnixTime // validityIntervalStart readonly to?: Time.UnixTime // ttl } + readonly requiredSigners: ReadonlyArray // Extra signers required (for script validation) } ``` diff --git a/packages/evolution/docs/modules/sdk/builders/TransactionResult.ts.md b/packages/evolution/docs/modules/sdk/builders/TransactionResult.ts.md index 0b8e3eb4..88b49320 100644 --- a/packages/evolution/docs/modules/sdk/builders/TransactionResult.ts.md +++ b/packages/evolution/docs/modules/sdk/builders/TransactionResult.ts.md @@ -1,6 +1,6 @@ --- title: sdk/builders/TransactionResult.ts -nav_order: 175 +nav_order: 176 parent: Modules --- diff --git a/packages/evolution/docs/modules/sdk/builders/TxBuilderImpl.ts.md b/packages/evolution/docs/modules/sdk/builders/TxBuilderImpl.ts.md index b7fd13a1..d8cac656 100644 --- a/packages/evolution/docs/modules/sdk/builders/TxBuilderImpl.ts.md +++ b/packages/evolution/docs/modules/sdk/builders/TxBuilderImpl.ts.md @@ -1,6 +1,6 @@ --- title: sdk/builders/TxBuilderImpl.ts -nav_order: 176 +nav_order: 177 parent: Modules --- @@ -62,7 +62,7 @@ export declare const assembleTransaction: ( inputs: ReadonlyArray, outputs: ReadonlyArray, fee: bigint -) => Effect.Effect +) => Effect.Effect ``` Added in v2.0.0 @@ -100,7 +100,7 @@ export declare const calculateMinimumUtxoLovelace: (params: { address: CoreAddress.Address assets: CoreAssets.Assets datum?: DatumOption.DatumOption - scriptRef?: TxOut.TransactionOutput["scriptRef"] + scriptRef?: CoreScript.Script coinsPerUtxoByte: bigint }) => Effect.Effect ``` @@ -349,7 +349,7 @@ export declare const makeTxOutput: (params: { address: CoreAddress.Address assets: CoreAssets.Assets datum?: DatumOption.DatumOption - scriptRef?: TxOut.TransactionOutput["scriptRef"] + scriptRef?: CoreScript.Script }) => Effect.Effect ``` diff --git a/packages/evolution/docs/modules/sdk/builders/Unfrack.ts.md b/packages/evolution/docs/modules/sdk/builders/Unfrack.ts.md index 4255b515..c18e2bb9 100644 --- a/packages/evolution/docs/modules/sdk/builders/Unfrack.ts.md +++ b/packages/evolution/docs/modules/sdk/builders/Unfrack.ts.md @@ -1,6 +1,6 @@ --- title: sdk/builders/Unfrack.ts -nav_order: 177 +nav_order: 178 parent: Modules --- diff --git a/packages/evolution/docs/modules/sdk/builders/operations/AddSigner.ts.md b/packages/evolution/docs/modules/sdk/builders/operations/AddSigner.ts.md new file mode 100644 index 00000000..d92b7b3e --- /dev/null +++ b/packages/evolution/docs/modules/sdk/builders/operations/AddSigner.ts.md @@ -0,0 +1,44 @@ +--- +title: sdk/builders/operations/AddSigner.ts +nav_order: 153 +parent: Modules +--- + +## AddSigner overview + +AddSigner operation - adds required signers to the transaction. + +Required signers are key hashes that must sign the transaction even if they +don't control any inputs. This is commonly used for scripts that check for +specific signers in their validation logic. + +Added in v2.0.0 + +--- + +

Table of contents

+ +- [programs](#programs) + - [createAddSignerProgram](#createaddsignerprogram) + +--- + +# programs + +## createAddSignerProgram + +Creates a ProgramStep for addSigner operation. +Adds a required signer (key hash) to the transaction. + +Implementation: + +1. Adds the key hash to the requiredSigners array in state +2. Deduplicates to avoid duplicate signers + +**Signature** + +```ts +export declare const createAddSignerProgram: (params: AddSignerParams) => 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 7a4de1ee..f8280ca1 100644 --- a/packages/evolution/docs/modules/sdk/builders/operations/Attach.ts.md +++ b/packages/evolution/docs/modules/sdk/builders/operations/Attach.ts.md @@ -1,6 +1,6 @@ --- title: sdk/builders/operations/Attach.ts -nav_order: 153 +nav_order: 154 parent: Modules --- diff --git a/packages/evolution/docs/modules/sdk/builders/operations/Collect.ts.md b/packages/evolution/docs/modules/sdk/builders/operations/Collect.ts.md index c41a18e5..c3931f3c 100644 --- a/packages/evolution/docs/modules/sdk/builders/operations/Collect.ts.md +++ b/packages/evolution/docs/modules/sdk/builders/operations/Collect.ts.md @@ -1,6 +1,6 @@ --- title: sdk/builders/operations/Collect.ts -nav_order: 154 +nav_order: 155 parent: Modules --- diff --git a/packages/evolution/docs/modules/sdk/builders/operations/Mint.ts.md b/packages/evolution/docs/modules/sdk/builders/operations/Mint.ts.md index 19b44756..26a51d95 100644 --- a/packages/evolution/docs/modules/sdk/builders/operations/Mint.ts.md +++ b/packages/evolution/docs/modules/sdk/builders/operations/Mint.ts.md @@ -1,6 +1,6 @@ --- title: sdk/builders/operations/Mint.ts -nav_order: 155 +nav_order: 156 parent: Modules --- diff --git a/packages/evolution/docs/modules/sdk/builders/operations/Operations.ts.md b/packages/evolution/docs/modules/sdk/builders/operations/Operations.ts.md index 27820779..e0ee1394 100644 --- a/packages/evolution/docs/modules/sdk/builders/operations/Operations.ts.md +++ b/packages/evolution/docs/modules/sdk/builders/operations/Operations.ts.md @@ -1,6 +1,6 @@ --- title: sdk/builders/operations/Operations.ts -nav_order: 156 +nav_order: 157 parent: Modules --- @@ -19,6 +19,8 @@ parent: Modules - [pool](#pool) - [RegisterPoolParams (interface)](#registerpoolparams-interface) - [RetirePoolParams (interface)](#retirepoolparams-interface) +- [signers](#signers) + - [AddSignerParams (interface)](#addsignerparams-interface) - [staking](#staking) - [DelegateToParams (interface)](#delegatetoparams-interface) - [DeregisterStakeParams (interface)](#deregisterstakeparams-interface) @@ -180,6 +182,26 @@ export interface RetirePoolParams { Added in v2.0.0 +# signers + +## AddSignerParams (interface) + +Parameters for adding a required signer to the transaction. + +Required signers must sign the transaction even if they don't control any inputs. +This is commonly used for scripts that check for specific signers in their validation logic. + +**Signature** + +```ts +export interface AddSignerParams { + /** The key hash that must sign the transaction */ + readonly keyHash: KeyHash.KeyHash +} +``` + +Added in v2.0.0 + # staking ## DelegateToParams (interface) @@ -367,7 +389,8 @@ export interface PayToAddressParams { readonly address: CoreAddress.Address readonly assets: CoreAssets.Assets readonly datum?: CoreDatumOption.DatumOption - readonly scriptRef?: CoreScriptRef.ScriptRef + /** Optional script to store as a reference script in the output */ + readonly script?: CoreScript.Script } ``` 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 d4aca6cd..d1f2f036 100644 --- a/packages/evolution/docs/modules/sdk/builders/operations/Pay.ts.md +++ b/packages/evolution/docs/modules/sdk/builders/operations/Pay.ts.md @@ -1,6 +1,6 @@ --- title: sdk/builders/operations/Pay.ts -nav_order: 157 +nav_order: 158 parent: Modules --- diff --git a/packages/evolution/docs/modules/sdk/builders/operations/ReadFrom.ts.md b/packages/evolution/docs/modules/sdk/builders/operations/ReadFrom.ts.md index 37543348..6714b5b2 100644 --- a/packages/evolution/docs/modules/sdk/builders/operations/ReadFrom.ts.md +++ b/packages/evolution/docs/modules/sdk/builders/operations/ReadFrom.ts.md @@ -1,6 +1,6 @@ --- title: sdk/builders/operations/ReadFrom.ts -nav_order: 158 +nav_order: 159 parent: Modules --- diff --git a/packages/evolution/docs/modules/sdk/builders/operations/Stake.ts.md b/packages/evolution/docs/modules/sdk/builders/operations/Stake.ts.md index 34b5f57a..bc1fb478 100644 --- a/packages/evolution/docs/modules/sdk/builders/operations/Stake.ts.md +++ b/packages/evolution/docs/modules/sdk/builders/operations/Stake.ts.md @@ -1,6 +1,6 @@ --- title: sdk/builders/operations/Stake.ts -nav_order: 159 +nav_order: 160 parent: Modules --- diff --git a/packages/evolution/docs/modules/sdk/builders/operations/Validity.ts.md b/packages/evolution/docs/modules/sdk/builders/operations/Validity.ts.md index c8569442..af698ffe 100644 --- a/packages/evolution/docs/modules/sdk/builders/operations/Validity.ts.md +++ b/packages/evolution/docs/modules/sdk/builders/operations/Validity.ts.md @@ -1,6 +1,6 @@ --- title: sdk/builders/operations/Validity.ts -nav_order: 160 +nav_order: 161 parent: Modules --- diff --git a/packages/evolution/docs/modules/sdk/builders/phases/Balance.ts.md b/packages/evolution/docs/modules/sdk/builders/phases/Balance.ts.md index 8e0ca6f4..7eadb686 100644 --- a/packages/evolution/docs/modules/sdk/builders/phases/Balance.ts.md +++ b/packages/evolution/docs/modules/sdk/builders/phases/Balance.ts.md @@ -1,6 +1,6 @@ --- title: sdk/builders/phases/Balance.ts -nav_order: 161 +nav_order: 162 parent: Modules --- diff --git a/packages/evolution/docs/modules/sdk/builders/phases/ChangeCreation.ts.md b/packages/evolution/docs/modules/sdk/builders/phases/ChangeCreation.ts.md index 80b8a125..eeb51172 100644 --- a/packages/evolution/docs/modules/sdk/builders/phases/ChangeCreation.ts.md +++ b/packages/evolution/docs/modules/sdk/builders/phases/ChangeCreation.ts.md @@ -1,6 +1,6 @@ --- title: sdk/builders/phases/ChangeCreation.ts -nav_order: 162 +nav_order: 163 parent: Modules --- diff --git a/packages/evolution/docs/modules/sdk/builders/phases/Collateral.ts.md b/packages/evolution/docs/modules/sdk/builders/phases/Collateral.ts.md index 7e968a88..ee157220 100644 --- a/packages/evolution/docs/modules/sdk/builders/phases/Collateral.ts.md +++ b/packages/evolution/docs/modules/sdk/builders/phases/Collateral.ts.md @@ -1,6 +1,6 @@ --- title: sdk/builders/phases/Collateral.ts -nav_order: 163 +nav_order: 164 parent: Modules --- diff --git a/packages/evolution/docs/modules/sdk/builders/phases/Evaluation.ts.md b/packages/evolution/docs/modules/sdk/builders/phases/Evaluation.ts.md index 361e9349..355d44ca 100644 --- a/packages/evolution/docs/modules/sdk/builders/phases/Evaluation.ts.md +++ b/packages/evolution/docs/modules/sdk/builders/phases/Evaluation.ts.md @@ -1,6 +1,6 @@ --- title: sdk/builders/phases/Evaluation.ts -nav_order: 164 +nav_order: 165 parent: Modules --- diff --git a/packages/evolution/docs/modules/sdk/builders/phases/Fallback.ts.md b/packages/evolution/docs/modules/sdk/builders/phases/Fallback.ts.md index 0b4e03cd..8f3a44ab 100644 --- a/packages/evolution/docs/modules/sdk/builders/phases/Fallback.ts.md +++ b/packages/evolution/docs/modules/sdk/builders/phases/Fallback.ts.md @@ -1,6 +1,6 @@ --- title: sdk/builders/phases/Fallback.ts -nav_order: 165 +nav_order: 166 parent: Modules --- 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 0ef7cebf..829bc75c 100644 --- a/packages/evolution/docs/modules/sdk/builders/phases/FeeCalculation.ts.md +++ b/packages/evolution/docs/modules/sdk/builders/phases/FeeCalculation.ts.md @@ -1,6 +1,6 @@ --- title: sdk/builders/phases/FeeCalculation.ts -nav_order: 166 +nav_order: 167 parent: Modules --- diff --git a/packages/evolution/docs/modules/sdk/builders/phases/Phases.ts.md b/packages/evolution/docs/modules/sdk/builders/phases/Phases.ts.md index 2518c4fd..58de8a3e 100644 --- a/packages/evolution/docs/modules/sdk/builders/phases/Phases.ts.md +++ b/packages/evolution/docs/modules/sdk/builders/phases/Phases.ts.md @@ -1,6 +1,6 @@ --- title: sdk/builders/phases/Phases.ts -nav_order: 167 +nav_order: 168 parent: Modules --- diff --git a/packages/evolution/docs/modules/sdk/builders/phases/Selection.ts.md b/packages/evolution/docs/modules/sdk/builders/phases/Selection.ts.md index 80c78c7c..2453dad1 100644 --- a/packages/evolution/docs/modules/sdk/builders/phases/Selection.ts.md +++ b/packages/evolution/docs/modules/sdk/builders/phases/Selection.ts.md @@ -1,6 +1,6 @@ --- title: sdk/builders/phases/Selection.ts -nav_order: 168 +nav_order: 169 parent: Modules --- diff --git a/packages/evolution/docs/modules/sdk/client/Client.ts.md b/packages/evolution/docs/modules/sdk/client/Client.ts.md index 50ab042c..ab3932fe 100644 --- a/packages/evolution/docs/modules/sdk/client/Client.ts.md +++ b/packages/evolution/docs/modules/sdk/client/Client.ts.md @@ -1,6 +1,6 @@ --- title: sdk/client/Client.ts -nav_order: 178 +nav_order: 179 parent: Modules --- diff --git a/packages/evolution/docs/modules/sdk/client/ClientImpl.ts.md b/packages/evolution/docs/modules/sdk/client/ClientImpl.ts.md index 9ec01459..b237495f 100644 --- a/packages/evolution/docs/modules/sdk/client/ClientImpl.ts.md +++ b/packages/evolution/docs/modules/sdk/client/ClientImpl.ts.md @@ -1,6 +1,6 @@ --- title: sdk/client/ClientImpl.ts -nav_order: 179 +nav_order: 180 parent: Modules --- diff --git a/packages/evolution/docs/modules/sdk/provider/Blockfrost.ts.md b/packages/evolution/docs/modules/sdk/provider/Blockfrost.ts.md index 83aed97b..71dd21d0 100644 --- a/packages/evolution/docs/modules/sdk/provider/Blockfrost.ts.md +++ b/packages/evolution/docs/modules/sdk/provider/Blockfrost.ts.md @@ -1,6 +1,6 @@ --- title: sdk/provider/Blockfrost.ts -nav_order: 189 +nav_order: 190 parent: Modules --- diff --git a/packages/evolution/docs/modules/sdk/provider/Koios.ts.md b/packages/evolution/docs/modules/sdk/provider/Koios.ts.md index 782220ca..414cf052 100644 --- a/packages/evolution/docs/modules/sdk/provider/Koios.ts.md +++ b/packages/evolution/docs/modules/sdk/provider/Koios.ts.md @@ -1,6 +1,6 @@ --- title: sdk/provider/Koios.ts -nav_order: 190 +nav_order: 191 parent: Modules --- diff --git a/packages/evolution/docs/modules/sdk/provider/Kupmios.ts.md b/packages/evolution/docs/modules/sdk/provider/Kupmios.ts.md index d94b8cea..754da591 100644 --- a/packages/evolution/docs/modules/sdk/provider/Kupmios.ts.md +++ b/packages/evolution/docs/modules/sdk/provider/Kupmios.ts.md @@ -1,6 +1,6 @@ --- title: sdk/provider/Kupmios.ts -nav_order: 191 +nav_order: 192 parent: Modules --- diff --git a/packages/evolution/docs/modules/sdk/provider/Maestro.ts.md b/packages/evolution/docs/modules/sdk/provider/Maestro.ts.md index 09ce9baf..6bf88a85 100644 --- a/packages/evolution/docs/modules/sdk/provider/Maestro.ts.md +++ b/packages/evolution/docs/modules/sdk/provider/Maestro.ts.md @@ -1,6 +1,6 @@ --- title: sdk/provider/Maestro.ts -nav_order: 192 +nav_order: 193 parent: Modules --- diff --git a/packages/evolution/docs/modules/sdk/provider/Provider.ts.md b/packages/evolution/docs/modules/sdk/provider/Provider.ts.md index 95f25c3a..5f7a13ce 100644 --- a/packages/evolution/docs/modules/sdk/provider/Provider.ts.md +++ b/packages/evolution/docs/modules/sdk/provider/Provider.ts.md @@ -1,6 +1,6 @@ --- title: sdk/provider/Provider.ts -nav_order: 193 +nav_order: 194 parent: Modules --- diff --git a/packages/evolution/docs/modules/sdk/wallet/Derivation.ts.md b/packages/evolution/docs/modules/sdk/wallet/Derivation.ts.md index 6dac78e2..63c4886d 100644 --- a/packages/evolution/docs/modules/sdk/wallet/Derivation.ts.md +++ b/packages/evolution/docs/modules/sdk/wallet/Derivation.ts.md @@ -1,6 +1,6 @@ --- title: sdk/wallet/Derivation.ts -nav_order: 200 +nav_order: 201 parent: Modules --- diff --git a/packages/evolution/docs/modules/sdk/wallet/WalletNew.ts.md b/packages/evolution/docs/modules/sdk/wallet/WalletNew.ts.md index 09af9c9b..a9f9df60 100644 --- a/packages/evolution/docs/modules/sdk/wallet/WalletNew.ts.md +++ b/packages/evolution/docs/modules/sdk/wallet/WalletNew.ts.md @@ -1,6 +1,6 @@ --- title: sdk/wallet/WalletNew.ts -nav_order: 201 +nav_order: 202 parent: Modules --- @@ -139,7 +139,7 @@ API wallets handle both signing and submission through the wallet extension. export interface ApiWalletEffect extends ReadOnlyWalletEffect { readonly signTx: ( tx: Transaction.Transaction | string, - context?: { utxos?: ReadonlyArray } + context?: { utxos?: ReadonlyArray; referenceUtxos?: ReadonlyArray } ) => Effect.Effect readonly signMessage: ( address: CoreAddress.Address | RewardAddress.RewardAddress, @@ -255,11 +255,12 @@ export interface SigningWalletEffect extends ReadOnlyWalletEffect { /** * Sign a transaction given its structured representation. UTxOs required for correctness * (e.g. to determine required signers) must be supplied by the caller (client) and not - * fetched internally. + * fetched internally. Reference UTxOs are used to extract required signers from native scripts + * that are used via reference inputs. */ readonly signTx: ( tx: Transaction.Transaction | string, - context?: { utxos?: ReadonlyArray } + context?: { utxos?: ReadonlyArray; referenceUtxos?: ReadonlyArray } ) => Effect.Effect readonly signMessage: ( address: CoreAddress.Address | RewardAddress.RewardAddress, diff --git a/packages/evolution/docs/modules/utils/FeeValidation.ts.md b/packages/evolution/docs/modules/utils/FeeValidation.ts.md index 6d502938..95d32e44 100644 --- a/packages/evolution/docs/modules/utils/FeeValidation.ts.md +++ b/packages/evolution/docs/modules/utils/FeeValidation.ts.md @@ -1,6 +1,6 @@ --- title: utils/FeeValidation.ts -nav_order: 203 +nav_order: 204 parent: Modules --- diff --git a/packages/evolution/docs/modules/utils/Hash.ts.md b/packages/evolution/docs/modules/utils/Hash.ts.md index 4554a5f5..0fdfa32b 100644 --- a/packages/evolution/docs/modules/utils/Hash.ts.md +++ b/packages/evolution/docs/modules/utils/Hash.ts.md @@ -1,6 +1,6 @@ --- title: utils/Hash.ts -nav_order: 204 +nav_order: 205 parent: Modules --- diff --git a/packages/evolution/docs/modules/utils/effect-runtime.ts.md b/packages/evolution/docs/modules/utils/effect-runtime.ts.md index 192b56c2..4683f013 100644 --- a/packages/evolution/docs/modules/utils/effect-runtime.ts.md +++ b/packages/evolution/docs/modules/utils/effect-runtime.ts.md @@ -1,6 +1,6 @@ --- title: utils/effect-runtime.ts -nav_order: 202 +nav_order: 203 parent: Modules --- diff --git a/packages/evolution/src/core/NativeScripts.ts b/packages/evolution/src/core/NativeScripts.ts index 0320f7da..5c8860c7 100644 --- a/packages/evolution/src/core/NativeScripts.ts +++ b/packages/evolution/src/core/NativeScripts.ts @@ -636,3 +636,35 @@ export const countRequiredSigners = (script: NativeScriptVariants): number => { return 0 } } +/** + * Extract all key hashes from a native script. + * Recursively traverses nested scripts to find all ScriptPubKey key hashes. + * + * @since 2.0.0 + * @category utilities + */ +export const extractKeyHashes = (script: NativeScriptVariants): ReadonlyArray => { + const keyHashes: Array = [] + + const traverse = (s: NativeScriptVariants): void => { + switch (s._tag) { + case "ScriptPubKey": + keyHashes.push(s.keyHash) + break + case "ScriptAll": + case "ScriptAny": + for (const nested of s.scripts) traverse(nested) + break + case "ScriptNOfK": + for (const nested of s.scripts) traverse(nested) + break + case "InvalidBefore": + case "InvalidHereafter": + // Time-based scripts don't contain key hashes + break + } + } + + traverse(script) + return keyHashes +} \ No newline at end of file diff --git a/packages/evolution/src/core/ScriptRef.ts b/packages/evolution/src/core/ScriptRef.ts index e0051412..a00a2da8 100644 --- a/packages/evolution/src/core/ScriptRef.ts +++ b/packages/evolution/src/core/ScriptRef.ts @@ -18,7 +18,7 @@ import * as Script from "./Script.js" * @category schemas */ export class ScriptRef extends Schema.TaggedClass()("ScriptRef", { - bytes: Schema.Uint8ArrayFromHex.pipe(Schema.filter((b) => b.length > 0)) + bytes: Schema.Uint8ArrayFromHex }) { toJSON() { return { diff --git a/packages/evolution/src/core/UTxO.ts b/packages/evolution/src/core/UTxO.ts index 9b419635..a060780c 100644 --- a/packages/evolution/src/core/UTxO.ts +++ b/packages/evolution/src/core/UTxO.ts @@ -11,7 +11,7 @@ import * as Bytes32 from "./Bytes32.js" import * as PlutusData from "./Data.js" import * as DatumOption from "./DatumOption.js" import * as Numeric from "./Numeric.js" -import * as ScriptRef from "./ScriptRef.js" +import * as Script from "./Script.js" import * as TransactionHash from "./TransactionHash.js" /** @@ -33,9 +33,14 @@ export class UTxO extends Schema.TaggedClass()("UTxO", { }) ), datumOption: Schema.optional(DatumOption.DatumOptionSchema), - scriptRef: Schema.optional(ScriptRef.ScriptRef) + scriptRef: Schema.optional(Script.Script) }) { toJSON() { + // Serialize Script to hex representation + const scriptRefJson = this.scriptRef + ? { _tag: this.scriptRef._tag, cbor: Script.toCBORHex(this.scriptRef) } + : undefined + return { _tag: this._tag, transactionId: this.transactionId.toJSON(), @@ -43,7 +48,7 @@ export class UTxO extends Schema.TaggedClass()("UTxO", { address: this.address.toJSON(), assets: this.assets.toJSON(), datumOption: this.datumOption?.toJSON(), - scriptRef: this.scriptRef?.toJSON() + scriptRef: scriptRefJson } } @@ -51,10 +56,6 @@ export class UTxO extends Schema.TaggedClass()("UTxO", { return Inspectable.format(this.toJSON()) } - [Inspectable.NodeInspectSymbol](): unknown { - return this.toJSON() - } - [Equal.symbol](that: unknown): boolean { return ( that instanceof UTxO && @@ -243,25 +244,35 @@ export const datumOptionToSDK = (datumOption: DatumOption.DatumOption): SDKDatum } /** - * Convert SDK Script to Core ScriptRef. + * Convert SDK Script to Core Script. * * @since 2.0.0 * @category conversions */ -export const scriptRefFromSDK = ( - script: SDKScript.Script -): Effect.Effect => - Schema.decodeUnknown(ScriptRef.FromHex)(script.script) +export const scriptFromSDK = ( + sdkScript: SDKScript.Script +): Effect.Effect => + Schema.decodeUnknown(Script.FromCBORHex())(sdkScript.script) /** - * Convert Core ScriptRef to SDK Script type string. - * Note: We lose the script type information as ScriptRef only stores bytes. + * Convert Core Script to SDK Script. * * @since 2.0.0 * @category conversions */ -export const scriptRefToSDKHex = (scriptRef: ScriptRef.ScriptRef): string => - Schema.encodeSync(Schema.Uint8ArrayFromHex)(scriptRef.bytes) +export const scriptToSDK = (script: Script.Script): SDKScript.Script => { + const hex = Script.toCBORHex(script) + switch (script._tag) { + case "NativeScript": + return { type: "Native", script: hex } + case "PlutusV1": + return { type: "PlutusV1", script: hex } + case "PlutusV2": + return { type: "PlutusV2", script: hex } + case "PlutusV3": + return { type: "PlutusV3", script: hex } + } +} /** * Convert SDK UTxO to Core UTxO. @@ -287,7 +298,7 @@ export const fromSDK = ( const datumOption = utxo.datumOption ? yield* datumOptionFromSDK(utxo.datumOption) : undefined // Convert script ref if present - const scriptRef = utxo.scriptRef ? yield* scriptRefFromSDK(utxo.scriptRef) : undefined + const scriptRef = utxo.scriptRef ? yield* scriptFromSDK(utxo.scriptRef) : undefined return new UTxO({ transactionId, @@ -314,12 +325,7 @@ export const toSDK = ( address: Schema.encodeSync(Address.FromBech32)(utxo.address), assets: fromCoreAssets(utxo.assets), datumOption: utxo.datumOption ? datumOptionToSDK(utxo.datumOption) : undefined, - scriptRef: utxo.scriptRef - ? { - type: "PlutusV3" as const, // Default type - we lose type info in ScriptRef - script: scriptRefToSDKHex(utxo.scriptRef) - } - : undefined + scriptRef: utxo.scriptRef ? scriptToSDK(utxo.scriptRef) : undefined }) /** diff --git a/packages/evolution/src/sdk/builders/SignBuilder.ts b/packages/evolution/src/sdk/builders/SignBuilder.ts index faa2198c..70372ed7 100644 --- a/packages/evolution/src/sdk/builders/SignBuilder.ts +++ b/packages/evolution/src/sdk/builders/SignBuilder.ts @@ -4,7 +4,7 @@ import type * as Transaction from "../../core/Transaction.js" import type * as TransactionWitnessSet from "../../core/TransactionWitnessSet.js" import type { EffectToPromiseAPI } from "../Type.js" import type { SubmitBuilder } from "./SubmitBuilder.js" -import type { TransactionBuilderError } from "./TransactionBuilder.js" +import type { ChainResult, TransactionBuilderError } from "./TransactionBuilder.js" import type { TransactionResultBase } from "./TransactionResult.js" // ============================================================================ @@ -27,6 +27,7 @@ export interface SignBuilderEffect { // Signing methods readonly sign: () => Effect.Effect + readonly signAndSubmit: () => Effect.Effect readonly signWithWitness: ( witnessSet: TransactionWitnessSet.TransactionWitnessSet ) => Effect.Effect @@ -43,9 +44,19 @@ export interface SignBuilderEffect { * Only available when the client has a signing wallet (seed, private key, or API wallet). * Provides access to unsigned transaction (via base interface) and signing operations. * + * Includes `chainResult` for transaction chaining - use `chainResult.available` as + * `availableUtxos` for the next transaction in a chain. + * * @since 2.0.0 * @category interfaces */ export interface SignBuilder extends TransactionResultBase, EffectToPromiseAPI { readonly Effect: SignBuilderEffect + /** + * Compute chain result for building dependent transactions. + * Contains consumed UTxOs, available UTxOs (remaining + created), and txHash. + * + * Result is memoized - computed once on first call, cached for subsequent calls. + */ + readonly chainResult: () => ChainResult } diff --git a/packages/evolution/src/sdk/builders/SignBuilderImpl.ts b/packages/evolution/src/sdk/builders/SignBuilderImpl.ts index dac49b98..f38b104f 100644 --- a/packages/evolution/src/sdk/builders/SignBuilderImpl.ts +++ b/packages/evolution/src/sdk/builders/SignBuilderImpl.ts @@ -16,14 +16,18 @@ import { Effect } from "effect" +import * as Script from "../../core/Script.js" import * as Transaction from "../../core/Transaction.js" +import * as TransactionHash from "../../core/TransactionHash.js" import * as TransactionWitnessSet from "../../core/TransactionWitnessSet.js" -import type * as CoreUTxO from "../../core/UTxO.js" +import type * as TxOut from "../../core/TxOut.js" +import * as CoreUTxO from "../../core/UTxO.js" +import { hashTransaction } from "../../utils/Hash.js" import type * as Provider from "../provider/Provider.js" import type * as WalletNew from "../wallet/WalletNew.js" import type { SignBuilder, SignBuilderEffect } from "./SignBuilder.js" import { makeSubmitBuilder } from "./SubmitBuilderImpl.js" -import { TransactionBuilderError } from "./TransactionBuilder.js" +import { type ChainResult, TransactionBuilderError } from "./TransactionBuilder.js" // ============================================================================ // SignBuilder Factory @@ -45,10 +49,41 @@ export const makeSignBuilder = (params: { transactionWithFakeWitnesses: Transaction.Transaction fee: bigint utxos: ReadonlyArray + referenceUtxos: ReadonlyArray provider: Provider.Provider wallet: Wallet + // Data for lazy chainResult computation + outputs: ReadonlyArray + availableUtxos: ReadonlyArray }): SignBuilder => { - const { fee, provider, transaction, transactionWithFakeWitnesses, utxos, wallet } = params + const { availableUtxos, fee, outputs, provider, referenceUtxos, transaction, transactionWithFakeWitnesses, utxos, wallet } = params + + // Memoized chainResult - computed once on first access + let _chainResult: ChainResult | undefined + const chainResult = (): ChainResult => { + if (_chainResult) return _chainResult + + const consumed = utxos + const txHash = hashTransaction(transaction.body) + + const created: Array = outputs.map((output, index) => + new CoreUTxO.UTxO({ + transactionId: txHash, + index: BigInt(index), + address: output.address, + assets: output.assets, + datumOption: output.datumOption, + scriptRef: output.scriptRef ? Script.fromCBOR(output.scriptRef.bytes) : undefined + }) + ) + + const consumedSet = new Set(consumed.map((u) => CoreUTxO.toOutRefString(u))) + const remaining = availableUtxos.filter((u) => !consumedSet.has(CoreUTxO.toOutRefString(u))) + const available = [...remaining, ...created] + + _chainResult = { consumed, available, txHash: TransactionHash.toHex(txHash) } + return _chainResult + } // ============================================================================ // Effect Namespace Implementation @@ -70,7 +105,7 @@ export const makeSignBuilder = (params: { yield* Effect.logDebug("Starting transaction signing (delegating to wallet Effect)") // Delegate to wallet's Effect.signTx with UTxO context - const walletWitnessSet = yield* wallet.Effect.signTx(transaction, { utxos }).pipe( + const walletWitnessSet = yield* wallet.Effect.signTx(transaction, { utxos, referenceUtxos }).pipe( Effect.mapError( (walletError) => new TransactionBuilderError({ message: "Failed to sign transaction", cause: walletError }) @@ -107,6 +142,18 @@ export const makeSignBuilder = (params: { return makeSubmitBuilder(signedTransaction, mergedWitnessSet, provider) }), + /** + * Sign and submit the transaction in one step. + * + * Convenience method that combines sign() and submit(). + * Returns the transaction hash on success. + */ + signAndSubmit: () => + Effect.gen(function* () { + const submitBuilder = yield* signEffect.sign() + return yield* submitBuilder.Effect.submit() + }), + /** * Sign the transaction using a pre-created witness set. * @@ -150,7 +197,8 @@ export const makeSignBuilder = (params: { Effect.gen(function* () { yield* Effect.logDebug(`Assembling transaction from ${witnesses.length} witness sets`) - // Merge all witness sets + // Start with the transaction's existing witness set (contains attached scripts, redeemers, etc.) + // Then merge in the provided witness sets (which contain signatures from multiple parties) const mergedWitnessSet = witnesses.reduce( (acc, ws) => new TransactionWitnessSet.TransactionWitnessSet({ vkeyWitnesses: [...(acc.vkeyWitnesses ?? []), ...(ws.vkeyWitnesses ?? [])], @@ -162,15 +210,16 @@ export const makeSignBuilder = (params: { plutusData: [...(acc.plutusData ?? []), ...(ws.plutusData ?? [])], redeemers: [...(acc.redeemers ?? [])] }), + // Start from transaction's witness set (NOT empty) to preserve attached scripts new TransactionWitnessSet.TransactionWitnessSet({ - vkeyWitnesses: [], - nativeScripts: [], - bootstrapWitnesses: [], - plutusV1Scripts: [], - plutusV2Scripts: [], - plutusV3Scripts: [], - plutusData: [], - redeemers: [] + vkeyWitnesses: transaction.witnessSet.vkeyWitnesses ?? [], + nativeScripts: transaction.witnessSet.nativeScripts ?? [], + bootstrapWitnesses: transaction.witnessSet.bootstrapWitnesses ?? [], + plutusV1Scripts: transaction.witnessSet.plutusV1Scripts ?? [], + plutusV2Scripts: transaction.witnessSet.plutusV2Scripts ?? [], + plutusV3Scripts: transaction.witnessSet.plutusV3Scripts ?? [], + plutusData: transaction.witnessSet.plutusData ?? [], + redeemers: transaction.witnessSet.redeemers ?? [] }) ) @@ -202,9 +251,10 @@ export const makeSignBuilder = (params: { partialSign: () => Effect.gen(function* () { yield* Effect.logDebug("Creating partial signature (delegating to wallet Effect)") + yield* Effect.logDebug(`[partialSign] referenceUtxos count: ${referenceUtxos.length}`) // Delegate to wallet's Effect.signTx to get witness set - const witnessSet = yield* wallet.Effect.signTx(transaction, { utxos }).pipe( + const witnessSet = yield* wallet.Effect.signTx(transaction, { utxos, referenceUtxos }).pipe( Effect.mapError( (walletError) => new TransactionBuilderError({ message: "Failed to create partial signature", cause: walletError }) @@ -232,7 +282,9 @@ export const makeSignBuilder = (params: { return { Effect: signEffect, + chainResult, sign: () => Effect.runPromise(signEffect.sign()), + signAndSubmit: () => Effect.runPromise(signEffect.signAndSubmit()), signWithWitness: (witnessSet: TransactionWitnessSet.TransactionWitnessSet) => Effect.runPromise(signEffect.signWithWitness(witnessSet)), assemble: (witnesses: ReadonlyArray) => diff --git a/packages/evolution/src/sdk/builders/TransactionBuilder.ts b/packages/evolution/src/sdk/builders/TransactionBuilder.ts index 4129595b..a4280b83 100644 --- a/packages/evolution/src/sdk/builders/TransactionBuilder.ts +++ b/packages/evolution/src/sdk/builders/TransactionBuilder.ts @@ -33,6 +33,7 @@ import * as CoreAssets from "../../core/Assets/index.js" import type * as Certificate from "../../core/Certificate.js" import type * as Coin from "../../core/Coin.js" import type * as PlutusData from "../../core/Data.js" +import type * as KeyHash from "../../core/KeyHash.js" import type * as Mint from "../../core/Mint.js" import type * as Network from "../../core/Network.js" import type * as RewardAccount from "../../core/RewardAccount.js" @@ -47,10 +48,11 @@ import type * as ProtocolParametersSDK from "../ProtocolParameters.js" import type * as Provider from "../provider/Provider.js" import type * as WalletNew from "../wallet/WalletNew.js" import type { CoinSelectionAlgorithm, CoinSelectionFunction } from "./CoinSelection.js" +import { createAddSignerProgram } from "./operations/AddSigner.js" import { attachScriptToState } from "./operations/Attach.js" import { createCollectFromProgram } from "./operations/Collect.js" import { createMintProgram } from "./operations/Mint.js" -import type { AuthCommitteeHotParams, CollectFromParams, DelegateToParams, DeregisterDRepParams, DeregisterStakeParams, MintTokensParams, PayToAddressParams, ReadFromParams, RegisterAndDelegateToParams, RegisterDRepParams, RegisterPoolParams, RegisterStakeParams, ResignCommitteeColdParams, RetirePoolParams, UpdateDRepParams, ValidityParams, WithdrawParams } from "./operations/Operations.js" +import type { AddSignerParams, 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" @@ -118,7 +120,8 @@ const initialTxBuilderState: TxBuilderState = { deferredRedeemers: new Map(), referenceInputs: [], certificates: [], - withdrawals: new Map() + withdrawals: new Map(), + requiredSigners: [] } /** @@ -274,7 +277,8 @@ const resolveSlotConfig = (config: TxBuilderConfig, options?: BuildOptions): Tim const assembleFinalResult = ( config: TxBuilderConfig, transaction: Transaction.Transaction, - txWithFakeWitnesses: Transaction.Transaction + txWithFakeWitnesses: Transaction.Transaction, + availableUtxos: ReadonlyArray ): Effect.Effect => Effect.gen(function* () { const buildCtxRef = yield* PhaseContextTag @@ -290,8 +294,12 @@ const assembleFinalResult = ( transactionWithFakeWitnesses: txWithFakeWitnesses, fee: buildCtx.calculatedFee, utxos: state.selectedUtxos, + referenceUtxos: state.referenceInputs, provider: config.provider!, - wallet + wallet, + // Pass raw data for lazy chainResult computation + outputs: state.outputs, + availableUtxos }) } @@ -452,7 +460,7 @@ const makeBuild = ( ) // Assemble and return final result - return yield* assembleFinalResult(config, transaction, txWithFakeWitnesses) + return yield* assembleFinalResult(config, transaction, txWithFakeWitnesses, availableUtxos) }).pipe( Effect.provideServiceEffect(TxContext, Ref.make(initialTxBuilderState)), Effect.provideService(BuildOptionsTag, { @@ -475,26 +483,6 @@ const makeBuild = ( ) ) -// Core Effect logic for chaining -const chainEffectCore = ( - config: TxBuilderConfig, - programs: Array, - _options: BuildOptions = DEFAULT_BUILD_OPTIONS -) => - Effect.gen(function* () { - // Chain logic: Execute programs and return intermediate state - return {} as ChainResult - }).pipe( - Effect.provideServiceEffect(TxContext, Ref.make(initialTxBuilderState)), - Effect.mapError( - (error) => - new TransactionBuilderError({ - message: "Chain failed", - cause: error - }) - ) - ) - // Core Effect logic for partial build const buildPartialEffectCore = ( config: TxBuilderConfig, @@ -522,18 +510,22 @@ const buildPartialEffectCore = ( /** * Result type for transaction chaining operations. * - * **NOTE: NOT YET IMPLEMENTED** - This interface is reserved for future implementation - * of multi-transaction workflows. Current chain methods return stub implementations. + * Provides consumed and available UTxOs for building chained transactions. + * The available UTxOs include both remaining unspent inputs AND newly created outputs + * with pre-computed txHash, ready to be spent in subsequent transactions. + * + * Accessed via `SignBuilder.chainResult()` after calling `build()`. * * @since 2.0.0 * @category model - * @experimental */ export interface ChainResult { - readonly transaction: Transaction.Transaction - readonly newOutputs: ReadonlyArray // UTxOs created by this transaction - readonly updatedUtxos: ReadonlyArray // Available UTxOs for next transaction (original - spent + new) - readonly spentUtxos: ReadonlyArray // UTxOs consumed by this transaction + /** UTxOs consumed from availableUtxos by coin selection */ + readonly consumed: ReadonlyArray + /** Available UTxOs: remaining unspent + newly created (with computed txHash) */ + readonly available: ReadonlyArray + /** Pre-computed transaction hash (blake2b-256 of transaction body) */ + readonly txHash: string } // ============================================================================ @@ -1286,6 +1278,7 @@ export interface TxBuilderState { readonly from?: Time.UnixTime // validityIntervalStart readonly to?: Time.UnixTime // ttl } + readonly requiredSigners: ReadonlyArray // Extra signers required (for script validation) } /** @@ -1872,6 +1865,71 @@ export interface TransactionBuilderBase { * @category validity-methods */ readonly setValidity: (params: ValidityParams) => this + + /** + * Add a required signer to the transaction. + * + * Adds a key hash to the transaction's requiredSigners field. This is used to + * require specific key signatures even when those keys don't control inputs. + * Common use cases include: + * - Multi-sig schemes requiring explicit signature verification + * - Plutus scripts that check for specific signers in the transaction + * - Governance transactions requiring DRep or committee member signatures + * + * Duplicate key hashes are automatically deduplicated. + * + * Queues a deferred operation that will be executed when build() is called. + * Returns the same builder for method chaining. + * + * @example + * ```typescript + * import * as KeyHash from "@evolution-sdk/core/KeyHash" + * import * as Address from "@evolution-sdk/core/Address" + * + * // Add signer from address credential + * const address = Address.fromBech32("addr_test1...") + * const cred = address.paymentCredential + * if (cred._tag === "KeyHash") { + * const tx = await builder + * .addSigner({ keyHash: cred }) + * .build() + * } + * ``` + * + * @since 2.0.0 + * @category builder-methods + */ + readonly addSigner: (params: AddSignerParams) => this + + // ============================================================================ + // Transaction Chaining Methods + // ============================================================================ + + /** + * Execute transaction build and return consumed/available UTxOs for chaining. + * + * Runs the full build pipeline (coin selection, fee calculation, evaluation) and returns + * which UTxOs were consumed and which remain available for subsequent transactions. + * Use this when building multiple dependent transactions in sequence. + * + * @returns Promise with consumed and available UTxOs + * + * @example + * ```typescript + * // Build first transaction, get remaining UTxOs + * const tx1 = await builder + * .payTo({ address, value: { lovelace: 5_000_000n } }) + * .build({ availableUtxos: walletUtxos }) + * + * // Build second transaction using remaining UTxOs from chainResult + * const tx2 = await builder + * .payTo({ address, value: { lovelace: 3_000_000n } }) + * .build({ availableUtxos: tx1.chainResult().available }) + * ``` + * + * @since 2.0.0 + * @category chaining-methods + */ } /** @@ -2141,6 +2199,10 @@ export function makeTxBuilder(config: TxBuilderConfig) { programs.push(createSetValidityProgram(params)) return txBuilder }, + addSigner: (params: AddSignerParams) => { + programs.push(createAddSignerProgram(params)) + return txBuilder + }, // ============================================================================ // Hybrid completion methods - Execute with fresh state @@ -2167,16 +2229,6 @@ export function makeTxBuilder(config: TxBuilderConfig) { ) }, - // ============================================================================ - // Transaction chaining methods - // ============================================================================ - - chainEffect: (options?: BuildOptions) => chainEffectCore(config, programs, options), - - chain: (options?: BuildOptions) => runEffectPromise(chainEffectCore(config, programs, options)), - - chainEither: (options?: BuildOptions) => runEffectPromise(chainEffectCore(config, programs, options).pipe(Effect.either)), - // ============================================================================ // Debug methods - Execute with fresh state, return partial transaction // ============================================================================ diff --git a/packages/evolution/src/sdk/builders/TxBuilderImpl.ts b/packages/evolution/src/sdk/builders/TxBuilderImpl.ts index 2bd906dc..5a41d1b1 100644 --- a/packages/evolution/src/sdk/builders/TxBuilderImpl.ts +++ b/packages/evolution/src/sdk/builders/TxBuilderImpl.ts @@ -12,6 +12,7 @@ import * as CostModel from "../../core/CostModel.js" import * as PlutusData from "../../core/Data.js" import * as DatumOption from "../../core/DatumOption.js" import * as Ed25519Signature from "../../core/Ed25519Signature.js" +import type * as KeyHash from "../../core/KeyHash.js" import * as NativeScripts from "../../core/NativeScripts.js" import type * as PlutusV1 from "../../core/PlutusV1.js" import type * as PlutusV2 from "../../core/PlutusV2.js" @@ -19,7 +20,9 @@ import type * as PlutusV3 from "../../core/PlutusV3.js" import * as PolicyId from "../../core/PolicyId.js" import * as Redeemer from "../../core/Redeemer.js" import type * as RewardAccount from "../../core/RewardAccount.js" +import * as CoreScript from "../../core/Script.js" import * as ScriptDataHash from "../../core/ScriptDataHash.js" +import * as ScriptRef from "../../core/ScriptRef.js" import * as Time from "../../core/Time/index.js" import * as Transaction from "../../core/Transaction.js" import * as TransactionBody from "../../core/TransactionBody.js" @@ -36,7 +39,7 @@ import * as Address from "../Address.js" import type * as Datum from "../Datum.js" // Internal imports import type { UnfrackOptions } from "./TransactionBuilder.js" -import { TransactionBuilderError, TxBuilderConfigTag,TxContext } from "./TransactionBuilder.js" +import { BuildOptionsTag, TransactionBuilderError, TxBuilderConfigTag,TxContext } from "./TransactionBuilder.js" import * as Unfrack from "./Unfrack.js" // ============================================================================ @@ -149,23 +152,26 @@ export const calculateReferenceScriptFee = ( referenceInputs: ReadonlyArray ): Effect.Effect => Effect.gen(function* () { - // Calculate total reference script size in bytes + // Calculate total reference script size in bytes (both native and Plutus) + // Per ADR 2024-08-14_009: "Native scripts that are used as reference scripts also contribute their size to this calculation" let totalScriptSize = 0 for (const utxo of referenceInputs) { if (utxo.scriptRef) { - // Get script CBOR bytes length from Core Script type - // Core scripts have a 'bytes' property with the raw script bytes - const scriptBytes = utxo.scriptRef.bytes.length + const scriptBytes = CoreScript.toCBOR(utxo.scriptRef).length totalScriptSize += scriptBytes + const scriptType = utxo.scriptRef._tag === "NativeScript" ? "Native" : "Plutus" + yield* Effect.logDebug(`[RefScriptFee] ${scriptType} script in ref input: ${scriptBytes} bytes`) } } - // No reference scripts = no fee + // No reference scripts = no tiered fee if (totalScriptSize === 0) { return 0n } + yield* Effect.logDebug(`[RefScriptFee] Total reference script size: ${totalScriptSize} bytes`) + // Check maximum size limit (200KB) if (totalScriptSize > 200_000) { return yield* Effect.fail( @@ -175,7 +181,7 @@ export const calculateReferenceScriptFee = ( ) } - // Calculate tiered fees + // Calculate tiered fees for all reference scripts let fee = 0n let remainingSize = totalScriptSize let tierIndex = 0 @@ -186,11 +192,14 @@ export const calculateReferenceScriptFee = ( const bytesInThisTier = Math.min(remainingSize, tierSize) const tierFee = BigInt(Math.ceil(bytesInThisTier * tierPrices[tierIndex]!)) fee += tierFee + yield* Effect.logDebug(`[RefScriptFee] Tier ${tierIndex + 1}: ${bytesInThisTier} bytes × ${tierPrices[tierIndex]} lovelace/byte = ${tierFee} lovelace`) remainingSize -= tierSize tierIndex++ } + yield* Effect.logDebug(`[RefScriptFee] Total tiered fee (Plutus only): ${fee} lovelace`) + return fee }) @@ -247,15 +256,20 @@ export const makeTxOutput = (params: { address: CoreAddress.Address assets: CoreAssets.Assets datum?: DatumOption.DatumOption - scriptRef?: TxOut.TransactionOutput["scriptRef"] + scriptRef?: CoreScript.Script }): Effect.Effect => Effect.gen(function* () { + // Convert Script to ScriptRef for CBOR encoding if provided + const scriptRefEncoded = params.scriptRef + ? new ScriptRef.ScriptRef({ bytes: CoreScript.toCBOR(params.scriptRef) }) + : undefined + // Create Core TransactionOutput directly with core types const output = new TxOut.TransactionOutput({ address: params.address, assets: params.assets, datumOption: params.datum, - scriptRef: params.scriptRef + scriptRef: scriptRefEncoded }) return output @@ -282,15 +296,20 @@ export const txOutputToTransactionOutput = (params: { address: CoreAddress.Address assets: CoreAssets.Assets datum?: DatumOption.DatumOption - scriptRef?: TxOut.TransactionOutput["scriptRef"] + scriptRef?: CoreScript.Script }): Effect.Effect => Effect.gen(function* () { + // Convert Script to ScriptRef for CBOR encoding if provided + const scriptRefEncoded = params.scriptRef + ? new ScriptRef.ScriptRef({ bytes: CoreScript.toCBOR(params.scriptRef) }) + : undefined + // Create TransactionOutput directly with core types const output = new TxOut.TransactionOutput({ address: params.address, assets: params.assets, datumOption: params.datum, - scriptRef: params.scriptRef + scriptRef: scriptRefEncoded }) return output @@ -449,7 +468,7 @@ export const assembleTransaction = ( inputs: ReadonlyArray, outputs: ReadonlyArray, fee: bigint -): Effect.Effect => +): Effect.Effect => Effect.gen(function* () { // Get state ref to access scripts and redeemers const stateRef = yield* TxContext @@ -727,8 +746,9 @@ export const assembleTransaction = ( : undefined // Convert validity interval from Unix time to slots - const config = yield* TxBuilderConfigTag - const slotConfig = Time.SLOT_CONFIG_NETWORK[config.network ?? "Mainnet"] + // Use resolved slot config from BuildOptionsTag (respects BuildOptions > TxBuilderConfig > network default priority) + const buildOptions = yield* BuildOptionsTag + const slotConfig = buildOptions.slotConfig! let ttl: bigint | undefined let validityIntervalStart: bigint | undefined @@ -742,6 +762,16 @@ export const assembleTransaction = ( yield* Effect.logDebug(`[Assembly] Validity start: ${validityIntervalStart} (from unix ${state.validity.from})`) } + // Build required signers (NonEmptyArray or undefined) + const requiredSigners = + state.requiredSigners.length > 0 + ? (state.requiredSigners as [KeyHash.KeyHash, ...Array]) + : undefined + + if (requiredSigners) { + yield* Effect.logDebug(`[Assembly] Required signers: ${requiredSigners.length}`) + } + const body = new TransactionBody.TransactionBody({ inputs: inputs as Array, outputs: transactionOutputs, @@ -755,7 +785,8 @@ export const assembleTransaction = ( mint: state.mint && state.mint.map.size > 0 ? state.mint : undefined, // Mint field from minting operations scriptDataHash, // Hash of redeemers + datums + cost models (required for Plutus scripts) certificates, // Certificates for staking operations - withdrawals // Withdrawals for claiming staking rewards + withdrawals, // Withdrawals for claiming staking rewards + requiredSigners // Extra signers required for script validation }) // Create witness set with scripts and redeemers @@ -969,29 +1000,32 @@ export const buildFakeWitnessSet = ( const plutusV2Scripts: Array = [] const plutusV3Scripts: Array = [] + // Helper to add dummy witnesses for native script required signers + const addNativeScriptWitnesses = (script: NativeScripts.NativeScript) => { + const requiredSigners = NativeScripts.countRequiredSigners(script.script) + for (let i = 0; i < requiredSigners; i++) { + const dummyKeyHash = new Uint8Array(28) + // Fill with unique pattern: 0xFF prefix + counter to distinguish from real keys + dummyKeyHash[0] = 0xFF + dummyKeyHash[1] = (keyHashesSet.size + i) & 0xFF + const dummyHashHex = Bytes.toHex(dummyKeyHash) + + // Only add if not already in the set + if (!keyHashesSet.has(dummyHashHex)) { + keyHashesSet.add(dummyHashHex) + keyHashes.push(dummyKeyHash) + } + } + return requiredSigners + } + for (const script of state.scripts.values()) { switch (script._tag) { case "NativeScript": { nativeScripts.push(script) // Count required signers for this native script and add fake witnesses - const requiredSigners = NativeScripts.countRequiredSigners(script.script) - yield* Effect.logInfo(`[buildFakeWitnessSet] Native script requires ${requiredSigners} signers`) - - // Add fake witnesses for each required signer - // Use unique dummy key hashes to avoid duplicates with input witnesses - for (let i = 0; i < requiredSigners; i++) { - const dummyKeyHash = new Uint8Array(28) - // Fill with unique pattern: 0xFF prefix + counter to distinguish from real keys - dummyKeyHash[0] = 0xFF - dummyKeyHash[1] = (keyHashesSet.size + i) & 0xFF - const dummyHashHex = Bytes.toHex(dummyKeyHash) - - // Only add if not already in the set - if (!keyHashesSet.has(dummyHashHex)) { - keyHashesSet.add(dummyHashHex) - keyHashes.push(dummyKeyHash) - } - } + const requiredSigners = addNativeScriptWitnesses(script) + yield* Effect.logDebug(`[buildFakeWitnessSet] Native script requires ${requiredSigners} signers`) break } case "PlutusV1": @@ -1006,6 +1040,14 @@ export const buildFakeWitnessSet = ( } } + // Also count required signers from reference scripts (scripts in referenceInputs) + for (const refUtxo of state.referenceInputs) { + if (refUtxo.scriptRef && refUtxo.scriptRef._tag === "NativeScript") { + const requiredSigners = addNativeScriptWitnesses(refUtxo.scriptRef) + yield* Effect.logDebug(`[buildFakeWitnessSet] Reference native script requires ${requiredSigners} signers`) + } + } + // Build fake witnesses for each unique key hash (inputs + native script signers) const vkeyWitnesses: Array = [] for (const keyHash of keyHashes) { @@ -1047,6 +1089,17 @@ export const buildFakeWitnessSet = ( } } + // Add fake witnesses for required signers (from addSigner operation) + // These key hashes are explicitly required to sign the transaction + for (const keyHash of state.requiredSigners) { + const hashHex = Bytes.toHex(keyHash.hash) + if (!keyHashesSet.has(hashHex)) { + keyHashesSet.add(hashHex) + const witness = yield* buildFakeVKeyWitness(keyHash.hash) + vkeyWitnesses.push(witness) + } + } + // Build fake redeemers from state.redeemers for accurate size estimation // Redeemers contribute to transaction size and must be included in fee calculation const fakeRedeemers: Array = [] @@ -1173,6 +1226,24 @@ export const calculateFeeIteratively = ( ? new Withdrawals.Withdrawals({ withdrawals: state.withdrawals }) : undefined + // Build requiredSigners for size estimation (NonEmptyArray or undefined) + const requiredSigners = state.requiredSigners.length > 0 + ? (state.requiredSigners as [KeyHash.KeyHash, ...Array]) + : undefined + + // Build referenceInputs for size estimation + // Reference inputs add to transaction size and must be included in fee calculation + let referenceInputsForFee: + | readonly [TransactionInput.TransactionInput, ...Array] + | undefined + if (state.referenceInputs.length > 0) { + const refInputs = yield* buildTransactionInputs(state.referenceInputs) + referenceInputsForFee = refInputs as readonly [ + TransactionInput.TransactionInput, + ...Array, + ] + } + while (iterations < maxIterations) { // Build transaction with current fee estimate const body = new TransactionBody.TransactionBody({ @@ -1185,7 +1256,9 @@ export const calculateFeeIteratively = ( collateralReturn, // Include collateral return for accurate size totalCollateral, // Include total collateral for accurate size certificates, // Include certificates for accurate size calculation - withdrawals // Include withdrawals for accurate size calculation + withdrawals, // Include withdrawals for accurate size calculation + requiredSigners, // Include requiredSigners for accurate size calculation + referenceInputs: referenceInputsForFee // Include reference inputs for accurate size calculation }) const transaction = new Transaction.Transaction({ @@ -1197,9 +1270,20 @@ export const calculateFeeIteratively = ( // Calculate size const size = yield* calculateTransactionSize(transaction) + + // Add reference script sizes to transaction size for base fee calculation + // Despite ADR docs, actual node behavior includes ref scripts in tx size for base fee + let refScriptSize = 0 + for (const utxo of state.referenceInputs) { + if (utxo.scriptRef) { + const scriptBytes = CoreScript.toCBOR(utxo.scriptRef).length + refScriptSize += scriptBytes + } + } + const sizeWithRefScripts = size + refScriptSize - // Calculate base fee based on size - const baseFee = calculateMinimumFee(size, { + // Calculate base fee based on size including reference scripts + const baseFee = calculateMinimumFee(sizeWithRefScripts, { minFeeCoefficient: protocolParams.minFeeCoefficient, minFeeConstant: protocolParams.minFeeConstant }) @@ -1406,7 +1490,7 @@ export const calculateMinimumUtxoLovelace = (params: { address: CoreAddress.Address assets: CoreAssets.Assets datum?: DatumOption.DatumOption - scriptRef?: TxOut.TransactionOutput["scriptRef"] + scriptRef?: CoreScript.Script coinsPerUtxoByte: bigint }): Effect.Effect => Effect.gen(function* () { diff --git a/packages/evolution/src/sdk/builders/operations/AddSigner.ts b/packages/evolution/src/sdk/builders/operations/AddSigner.ts new file mode 100644 index 00000000..c9aa16e8 --- /dev/null +++ b/packages/evolution/src/sdk/builders/operations/AddSigner.ts @@ -0,0 +1,47 @@ +/** + * AddSigner operation - adds required signers to the transaction. + * + * Required signers are key hashes that must sign the transaction even if they + * don't control any inputs. This is commonly used for scripts that check for + * specific signers in their validation logic. + * + * @module operations/AddSigner + * @since 2.0.0 + */ + +import { Effect, Equal, Ref } from "effect" + +import { TxContext } from "../TransactionBuilder.js" +import type { AddSignerParams } from "./Operations.js" + +/** + * Creates a ProgramStep for addSigner operation. + * Adds a required signer (key hash) to the transaction. + * + * Implementation: + * 1. Adds the key hash to the requiredSigners array in state + * 2. Deduplicates to avoid duplicate signers + * + * @since 2.0.0 + * @category programs + */ +export const createAddSignerProgram = (params: AddSignerParams) => + Effect.gen(function* () { + const ctx = yield* TxContext + + yield* Ref.update(ctx, (state) => { + // Check if this key hash is already in requiredSigners (deduplicate) + const alreadyExists = state.requiredSigners.some((existing) => Equal.equals(existing, params.keyHash)) + + if (alreadyExists) { + return state + } + + return { + ...state, + requiredSigners: [...state.requiredSigners, params.keyHash] + } + }) + + yield* Effect.logDebug(`[AddSigner] Added required signer`) + }) diff --git a/packages/evolution/src/sdk/builders/operations/Collect.ts b/packages/evolution/src/sdk/builders/operations/Collect.ts index 2359b4fe..dda6d0a4 100644 --- a/packages/evolution/src/sdk/builders/operations/Collect.ts +++ b/packages/evolution/src/sdk/builders/operations/Collect.ts @@ -8,6 +8,7 @@ import { Effect, Ref } from "effect" import * as CoreAssets from "../../../core/Assets/index.js" +import * as ScriptHash from "../../../core/ScriptHash.js" import * as UTxO from "../../../core/UTxO.js" import * as RedeemerBuilder from "../RedeemerBuilder.js" import { TransactionBuilderError, TxContext } from "../TransactionBuilder.js" @@ -37,6 +38,7 @@ import type { CollectFromParams } from "./Operations.js" export const createCollectFromProgram = (params: CollectFromParams) => Effect.gen(function* () { const ctx = yield* TxContext + const state = yield* Ref.get(ctx) // 1. Validate inputs exist if (params.inputs.length === 0) { @@ -50,16 +52,42 @@ export const createCollectFromProgram = (params: CollectFromParams) => // 2. Filter script-locked UTxOs const scriptUtxos = yield* filterScriptUtxos(params.inputs) - // 3. Validate redeemer for script UTxOs - if (scriptUtxos.length > 0 && !params.redeemer) { + // 3. Filter out native script UTxOs (those with native scripts don't need redeemers) + // Native scripts are validated by signatures, not redeemers + // Check: attached scripts, inline scriptRef, and reference inputs + const plutusScriptUtxos = scriptUtxos.filter((utxo) => { + const credential = utxo.address.paymentCredential + if (credential?._tag !== "ScriptHash") return false + + const scriptHashHex = ScriptHash.toHex(credential) + + // Check 1: Script attached via attachScript() + const attachedScript = state.scripts.get(scriptHashHex) + if (attachedScript?._tag === "NativeScript") return false + + // Check 2: Script inline in the UTxO being spent + if (utxo.scriptRef?._tag === "NativeScript") return false + + // Check 3: Script available via reference input + const refScript = state.referenceInputs.find((ref) => + ref.scriptRef && ScriptHash.toHex(ScriptHash.fromScript(ref.scriptRef)) === scriptHashHex + ) + if (refScript?.scriptRef?._tag === "NativeScript") return false + + // Otherwise it's a Plutus script (or script not found) + return true + }) + + // 4. Validate redeemer for Plutus script UTxOs only + if (plutusScriptUtxos.length > 0 && !params.redeemer) { return yield* Effect.fail( new TransactionBuilderError({ - message: `Redeemer required for ${scriptUtxos.length} script-locked UTxO(s)` + message: `Redeemer required for ${plutusScriptUtxos.length} script-locked UTxO(s)` }) ) } - // 4. Add UTxOs to selected inputs and track redeemers and input assets + // 5. Add UTxOs to selected inputs and track redeemers and input assets const inputAssets = calculateTotalAssets(params.inputs) yield* Ref.update(ctx, (state) => { diff --git a/packages/evolution/src/sdk/builders/operations/Operations.ts b/packages/evolution/src/sdk/builders/operations/Operations.ts index ea3b670a..91ef2051 100644 --- a/packages/evolution/src/sdk/builders/operations/Operations.ts +++ b/packages/evolution/src/sdk/builders/operations/Operations.ts @@ -5,9 +5,10 @@ import type * as Credential from "../../../core/Credential.js" import type * as CoreDatumOption from "../../../core/DatumOption.js" import type * as DRep from "../../../core/DRep.js" import type * as EpochNo from "../../../core/EpochNo.js" +import type * as KeyHash from "../../../core/KeyHash.js" import type * as PoolKeyHash from "../../../core/PoolKeyHash.js" import type * as PoolParams from "../../../core/PoolParams.js" -import type * as CoreScriptRef from "../../../core/ScriptRef.js" +import type * as CoreScript from "../../../core/Script.js" import type * as Time from "../../../core/Time/index.js" import type * as UTxO from "../../../core/UTxO.js" import type * as RedeemerBuilder from "../RedeemerBuilder.js" @@ -39,7 +40,8 @@ export interface PayToAddressParams { readonly address: CoreAddress.Address readonly assets: CoreAssets.Assets readonly datum?: CoreDatumOption.DatumOption - readonly scriptRef?: CoreScriptRef.ScriptRef + /** Optional script to store as a reference script in the output */ + readonly script?: CoreScript.Script } /** @@ -318,4 +320,22 @@ export interface RetirePoolParams { readonly poolKeyHash: PoolKeyHash.PoolKeyHash /** Epoch at which retirement takes effect */ readonly epoch: EpochNo.EpochNo +} + +// ============================================================================ +// Required Signers +// ============================================================================ + +/** + * Parameters for adding a required signer to the transaction. + * + * Required signers must sign the transaction even if they don't control any inputs. + * This is commonly used for scripts that check for specific signers in their validation logic. + * + * @since 2.0.0 + * @category signers + */ +export interface AddSignerParams { + /** The key hash that must sign the transaction */ + readonly keyHash: KeyHash.KeyHash } \ No newline at end of file diff --git a/packages/evolution/src/sdk/builders/operations/Pay.ts b/packages/evolution/src/sdk/builders/operations/Pay.ts index 2898dc69..d885add2 100644 --- a/packages/evolution/src/sdk/builders/operations/Pay.ts +++ b/packages/evolution/src/sdk/builders/operations/Pay.ts @@ -33,7 +33,7 @@ export const createPayToAddressProgram = (params: PayToAddressParams) => address: params.address, assets: params.assets, datum: params.datum, - scriptRef: params.scriptRef + scriptRef: params.script // Script is now directly compatible with UTxO.scriptRef }) // 2. Add output to state diff --git a/packages/evolution/src/sdk/builders/operations/index.ts b/packages/evolution/src/sdk/builders/operations/index.ts index 79cde146..21d06c3d 100644 --- a/packages/evolution/src/sdk/builders/operations/index.ts +++ b/packages/evolution/src/sdk/builders/operations/index.ts @@ -1,3 +1,4 @@ +export * from "./AddSigner.js" export * from "./Attach.js" export * from "./Collect.js" export * from "./Operations.js" diff --git a/packages/evolution/src/sdk/builders/phases/FeeCalculation.ts b/packages/evolution/src/sdk/builders/phases/FeeCalculation.ts index 35ca7a82..b2180a2c 100644 --- a/packages/evolution/src/sdk/builders/phases/FeeCalculation.ts +++ b/packages/evolution/src/sdk/builders/phases/FeeCalculation.ts @@ -85,14 +85,14 @@ export const executeFeeCalculation = (): Effect.Effect< priceStep: protocolParams.priceStep }) - yield* Effect.logDebug(`[FeeCalculation] Base fee: ${baseFee}`) + yield* Effect.logDebug(`[FeeCalculation] Base fee (includes ref script size): ${baseFee}`) - // Step 4a: Add reference script fee if reference inputs are present + // Step 4a: Add tiered reference script fee for all reference scripts const refScriptFee = yield* calculateReferenceScriptFee(state.referenceInputs) - yield* Effect.logDebug(`[FeeCalculation] Reference script fee: ${refScriptFee}`) + yield* Effect.logDebug(`[FeeCalculation] Tiered reference script fee: ${refScriptFee}`) const calculatedFee = baseFee + refScriptFee - yield* Effect.logDebug(`[FeeCalculation] Total fee (base + refScript): ${calculatedFee}`) + yield* Effect.logDebug(`[FeeCalculation] Total fee: ${calculatedFee}`) // Step 5: Calculate leftover after fee NOW (after fee is known) const inputAssets = state.totalInputAssets diff --git a/packages/evolution/src/sdk/client/ClientImpl.ts b/packages/evolution/src/sdk/client/ClientImpl.ts index 542c6866..0ae42917 100644 --- a/packages/evolution/src/sdk/client/ClientImpl.ts +++ b/packages/evolution/src/sdk/client/ClientImpl.ts @@ -1,7 +1,9 @@ import { Effect, Equal, ParseResult } from "effect" import * as CoreAddress from "../../core/Address.js" +import * as Bytes from "../../core/Bytes.js" import * as KeyHash from "../../core/KeyHash.js" +import type * as NativeScripts from "../../core/NativeScripts.js" import * as PrivateKey from "../../core/PrivateKey.js" import type * as Time from "../../core/Time/index.js" import * as Transaction from "../../core/Transaction.js" @@ -16,6 +18,7 @@ import { type ReadOnlyTransactionBuilder, type SigningTransactionBuilder } from "../builders/TransactionBuilder.js" +import * as OutRef from "../OutRef.js" import * as Blockfrost from "../provider/Blockfrost.js" import * as Koios from "../provider/Koios.js" import * as Kupmios from "../provider/Kupmios.js" @@ -207,7 +210,41 @@ const createReadOnlyClient = ( } /** - * Determine key hashes that must sign a transaction based on inputs, withdrawals, and certificates. + * Extract all key hashes from a native script (recursively). + * This traverses ALL, ANY, and N-of-K scripts to find all ScriptPubKey key hashes. + * + * @since 2.0.0 + * @category utilities + */ +const extractKeyHashesFromNativeScript = (script: NativeScripts.NativeScriptVariants): Set => { + const keyHashes = new Set() + + const traverse = (s: NativeScripts.NativeScriptVariants): void => { + switch (s._tag) { + case "ScriptPubKey": + keyHashes.add(Bytes.toHex(s.keyHash)) + break + case "ScriptAll": + case "ScriptAny": + for (const nested of s.scripts) traverse(nested) + break + case "ScriptNOfK": + for (const nested of s.scripts) traverse(nested) + break + case "InvalidBefore": + case "InvalidHereafter": + // Time-based scripts don't contain key hashes + break + } + } + + traverse(script) + return keyHashes +} + +/** + * Determine key hashes that must sign a transaction based on inputs, withdrawals, certificates, + * and native scripts attached to the transaction or in reference inputs. * * @since 2.0.0 * @category predicates @@ -218,6 +255,7 @@ const computeRequiredKeyHashesSync = (params: { stakeKhHex?: string tx: Transaction.Transaction utxos: ReadonlyArray + referenceUtxos?: ReadonlyArray }): Set => { const required = new Set() @@ -225,6 +263,24 @@ const computeRequiredKeyHashesSync = (params: { for (const kh of params.tx.body.requiredSigners) required.add(KeyHash.toHex(kh)) } + // Extract key hashes from native scripts in the witness set + if (params.tx.witnessSet.nativeScripts) { + for (const nativeScript of params.tx.witnessSet.nativeScripts) { + const scriptKeyHashes = extractKeyHashesFromNativeScript(nativeScript.script) + for (const kh of scriptKeyHashes) required.add(kh) + } + } + + // Extract key hashes from native scripts in reference inputs + if (params.referenceUtxos) { + for (const utxo of params.referenceUtxos) { + if (utxo.scriptRef && utxo.scriptRef._tag === "NativeScript") { + const scriptKeyHashes = extractKeyHashesFromNativeScript(utxo.scriptRef.script) + for (const kh of scriptKeyHashes) required.add(kh) + } + } + } + const ownedRefs = new Set(params.utxos.map((u) => CoreUTxO.toOutRefString(u))) const checkInputs = (inputs?: ReadonlyArray) => { @@ -290,7 +346,7 @@ const createSigningWallet = (network: WalletNew.Network, config: SeedWalletConfi const effectInterface: WalletNew.SigningWalletEffect = { address: () => Effect.map(derivationEffect, (d) => d.address), rewardAddress: () => Effect.map(derivationEffect, (d) => d.rewardAddress ?? null), - signTx: (txOrHex: Transaction.Transaction | string, context?: { utxos?: ReadonlyArray }) => + signTx: (txOrHex: Transaction.Transaction | string, context?: { utxos?: ReadonlyArray; referenceUtxos?: ReadonlyArray }) => Effect.gen(function* () { const derivation = yield* derivationEffect @@ -303,6 +359,7 @@ const createSigningWallet = (network: WalletNew.Network, config: SeedWalletConfi ) : txOrHex const utxos = context?.utxos ?? [] + const referenceUtxos = context?.referenceUtxos ?? [] // Determine required key hashes for signing const required = computeRequiredKeyHashesSync({ @@ -310,7 +367,8 @@ const createSigningWallet = (network: WalletNew.Network, config: SeedWalletConfi rewardAddress: derivation.rewardAddress ?? null, stakeKhHex: derivation.stakeKhHex, tx, - utxos + utxos, + referenceUtxos }) // Build witnesses for keys we have @@ -374,7 +432,7 @@ const createPrivateKeyWallet = ( const effectInterface: WalletNew.SigningWalletEffect = { address: () => Effect.map(derivationEffect, (d) => d.address), rewardAddress: () => Effect.map(derivationEffect, (d) => d.rewardAddress ?? null), - signTx: (txOrHex: Transaction.Transaction | string, context?: { utxos?: ReadonlyArray }) => + signTx: (txOrHex: Transaction.Transaction | string, context?: { utxos?: ReadonlyArray; referenceUtxos?: ReadonlyArray }) => Effect.gen(function* () { const derivation = yield* derivationEffect @@ -387,13 +445,15 @@ const createPrivateKeyWallet = ( ) : txOrHex const utxos = context?.utxos ?? [] + const referenceUtxos = context?.referenceUtxos ?? [] const required = computeRequiredKeyHashesSync({ paymentKhHex: derivation.paymentKhHex, rewardAddress: derivation.rewardAddress ?? null, stakeKhHex: derivation.stakeKhHex, tx, - utxos + utxos, + referenceUtxos }) const txHash = hashTransaction(tx.body) @@ -591,9 +651,46 @@ const createSigningClient = ( ? createPrivateKeyWallet(walletNetwork, walletConfig) : createApiWallet(walletNetwork, walletConfig) + // Enhanced signTx that automatically fetches reference UTxOs from the network + const signTxWithAutoFetch = ( + txOrHex: Transaction.Transaction | string, + context?: { utxos?: ReadonlyArray; referenceUtxos?: ReadonlyArray } + ): Effect.Effect => + Effect.gen(function* () { + const tx = + typeof txOrHex === "string" + ? yield* ParseResult.decodeUnknownEither(Transaction.FromCBORHex())(txOrHex).pipe( + Effect.mapError( + (cause) => new WalletNew.WalletError({ message: `Failed to decode transaction: ${cause}`, cause }) + ) + ) + : txOrHex + + // If referenceUtxos already provided, use them directly + if (context?.referenceUtxos && context.referenceUtxos.length > 0) { + return yield* wallet.Effect.signTx(tx, context) + } + + // Auto-fetch reference UTxOs from the network if the transaction has reference inputs + let referenceUtxos: ReadonlyArray = [] + if (tx.body.referenceInputs && tx.body.referenceInputs.length > 0) { + const outRefs = tx.body.referenceInputs.map((input) => + OutRef.make(TransactionHash.toHex(input.transactionId), Number(input.index)) + ) + // Fetch reference UTxOs from the provider + referenceUtxos = yield* provider.Effect.getUtxosByOutRef(outRefs).pipe( + Effect.mapError((e) => new WalletNew.WalletError({ message: `Failed to fetch reference UTxOs: ${e.message}`, cause: e })) + ) + } + + return yield* wallet.Effect.signTx(tx, { ...context, referenceUtxos }) + }) + const effectInterface = { ...wallet.Effect, ...provider.Effect, + // Override signTx with auto-fetch capability + signTx: signTxWithAutoFetch, getWalletUtxos: () => Effect.flatMap(wallet.Effect.address(), (addr) => provider.Effect.getUtxos(addr)), getWalletDelegation: () => Effect.flatMap(wallet.Effect.rewardAddress(), (rewardAddr) => { @@ -610,6 +707,9 @@ const createSigningClient = ( return { ...provider, ...wallet, + // Override signTx with auto-fetch capability (must come after ...wallet to override) + signTx: (txOrHex: Transaction.Transaction | string, context?: { utxos?: ReadonlyArray; referenceUtxos?: ReadonlyArray }) => + Effect.runPromise(signTxWithAutoFetch(txOrHex, context)), // Promise methods call Effect implementations getWalletUtxos, getWalletDelegation: () => Effect.runPromise(effectInterface.getWalletDelegation()), diff --git a/packages/evolution/src/sdk/provider/internal/KupmiosEffects.ts b/packages/evolution/src/sdk/provider/internal/KupmiosEffects.ts index 6dac0681..f7430491 100644 --- a/packages/evolution/src/sdk/provider/internal/KupmiosEffects.ts +++ b/packages/evolution/src/sdk/provider/internal/KupmiosEffects.ts @@ -6,7 +6,11 @@ import * as CoreAssets from "../../../core/Assets/index.js" import * as Bytes from "../../../core/Bytes.js" import * as PlutusData from "../../../core/Data.js" import * as DatumOption from "../../../core/DatumOption.js" -import * as ScriptRef from "../../../core/ScriptRef.js" +import * as NativeScripts from "../../../core/NativeScripts.js" +import * as PlutusV1 from "../../../core/PlutusV1.js" +import * as PlutusV2 from "../../../core/PlutusV2.js" +import * as PlutusV3 from "../../../core/PlutusV3.js" +import type * as CoreScript from "../../../core/Script.js" import * as TransactionHash from "../../../core/TransactionHash.js" import * as CoreUTxO from "../../../core/UTxO.js" import type * as Credential from "../../Credential.js" @@ -98,7 +102,7 @@ const retrieveDatumEffect = }) const getScriptEffect = - (kupoUrl: string, kupoHeader?: Record) => (script_hash: Kupo.UTxO["script_hash"]): Effect.Effect => + (kupoUrl: string, kupoHeader?: Record) => (script_hash: Kupo.UTxO["script_hash"]): Effect.Effect => Effect.gen(function* () { if (script_hash) { const pattern = `${kupoUrl}/scripts/${script_hash}` @@ -108,22 +112,31 @@ const getScriptEffect = Effect.flatMap(Effect.fromNullable), Effect.retry(Schedule.compose(Schedule.exponential(50), Schedule.recurs(5))), Effect.timeout(5_000), - Effect.map(({ language, script }) => { - // Apply appropriate CBOR encoding based on script type - let encodedScript: string + Effect.map(({ language, script }): CoreScript.Script => { + // Convert script hex to bytes + const rawScriptBytes = Bytes.fromHex(script) + + // Create the proper Script type based on language switch (language) { - case "native": - encodedScript = script // Native scripts don't need double encoding - break - case "plutus:v1": - case "plutus:v2": - case "plutus:v3": - encodedScript = Script.applyDoubleCborEncoding(script) - break + case "native": { + // Parse the native script from CBOR + return NativeScripts.fromCBORBytes(rawScriptBytes) + } + case "plutus:v1": { + const doubleCborHex = Script.applyDoubleCborEncoding(script) + return new PlutusV1.PlutusV1({ bytes: Bytes.fromHex(doubleCborHex) }) + } + case "plutus:v2": { + const doubleCborHex = Script.applyDoubleCborEncoding(script) + return new PlutusV2.PlutusV2({ bytes: Bytes.fromHex(doubleCborHex) }) + } + case "plutus:v3": { + const doubleCborHex = Script.applyDoubleCborEncoding(script) + return new PlutusV3.PlutusV3({ bytes: Bytes.fromHex(doubleCborHex) }) + } + default: + throw new Error(`Unknown script language: ${language}`) } - // Convert hex script to bytes and wrap in ScriptRef - const scriptBytes = Bytes.fromHex(encodedScript) - return new ScriptRef.ScriptRef({ bytes: scriptBytes }) }), Effect.catchAll((cause) => new ProviderError({ cause, message: "Failed to get script" })) ) @@ -285,7 +298,20 @@ export const submitTxEffect = (ogmiosUrl: string, headers?: { ogmiosHeader?: Rec const { result } = yield* pipe( HttpUtils.postJson(ogmiosUrl, data, schema, headers?.ogmiosHeader), Effect.timeout(TIMEOUT), - Effect.catchAll((cause) => new ProviderError({ cause, message: "Failed to submit transaction" })), + Effect.catchAll((cause) => { + // TODO: This is a workaround to extract meaningful error messages from Ogmios. + // Ogmios returns errors with a `description` field, but we don't have a proper + // error schema defined. The proper fix would be: + // 1. Define an OgmiosError schema that captures the actual error response shape + // 2. Use schema-validated error decoding instead of runtime duck-typing + // 3. Apply consistent error extraction across all provider methods + const errorMessage = cause instanceof Error + ? cause.message + : typeof cause === 'object' && cause !== null && 'description' in cause + ? String((cause as { description: unknown }).description) + : "Failed to submit transaction" + return Effect.fail(new ProviderError({ cause, message: errorMessage })) + }), Effect.provide(FetchHttpClient.layer) ) diff --git a/packages/evolution/src/sdk/provider/internal/Ogmios.ts b/packages/evolution/src/sdk/provider/internal/Ogmios.ts index 20040b75..12c3e16c 100644 --- a/packages/evolution/src/sdk/provider/internal/Ogmios.ts +++ b/packages/evolution/src/sdk/provider/internal/Ogmios.ts @@ -8,7 +8,7 @@ import * as Bytes from "../../../core/Bytes.js" import * as PlutusData from "../../../core/Data.js" import type * as DatumOption from "../../../core/DatumOption.js" import * as PolicyId from "../../../core/PolicyId.js" -import type * as ScriptRef from "../../../core/ScriptRef.js" +import * as CoreScript from "../../../core/Script.js" import * as TransactionHash from "../../../core/TransactionHash.js" import type * as CoreUTxO from "../../../core/UTxO.js" @@ -121,7 +121,7 @@ export const Delegation = Schema.NullOr( ) type Script = { - language: "plutus:v1" | "plutus:v2" | "plutus:v3" + language: "native" | "plutus:v1" | "plutus:v2" | "plutus:v3" cbor: string } @@ -156,12 +156,19 @@ export const toOgmiosUTxOs = (utxos: Array | undefined): Array { - if (scriptRef) { - // ScriptRef contains raw script bytes - we need to detect the language from the CBOR - // For now, we return undefined as we'd need to decode the script to determine language - // TODO: Implement script language detection from bytes - return undefined + const toOgmiosScript = (script: CoreScript.Script | undefined): OgmiosUTxO["script"] | undefined => { + if (script) { + // Script type directly tells us the language + switch (script._tag) { + case "NativeScript": + return { language: "native", cbor: CoreScript.toCBORHex(script) } + case "PlutusV1": + return { language: "plutus:v1", cbor: CoreScript.toCBORHex(script) } + case "PlutusV2": + return { language: "plutus:v2", cbor: CoreScript.toCBORHex(script) } + case "PlutusV3": + return { language: "plutus:v3", cbor: CoreScript.toCBORHex(script) } + } } return undefined } diff --git a/packages/evolution/src/sdk/wallet/WalletNew.ts b/packages/evolution/src/sdk/wallet/WalletNew.ts index 458b3960..02b03c52 100644 --- a/packages/evolution/src/sdk/wallet/WalletNew.ts +++ b/packages/evolution/src/sdk/wallet/WalletNew.ts @@ -82,11 +82,12 @@ export interface SigningWalletEffect extends ReadOnlyWalletEffect { /** * Sign a transaction given its structured representation. UTxOs required for correctness * (e.g. to determine required signers) must be supplied by the caller (client) and not - * fetched internally. + * fetched internally. Reference UTxOs are used to extract required signers from native scripts + * that are used via reference inputs. */ readonly signTx: ( tx: Transaction.Transaction | string, - context?: { utxos?: ReadonlyArray } + context?: { utxos?: ReadonlyArray; referenceUtxos?: ReadonlyArray } ) => Effect.Effect readonly signMessage: ( address: CoreAddress.Address | RewardAddress.RewardAddress, @@ -134,7 +135,7 @@ export interface WalletApi { export interface ApiWalletEffect extends ReadOnlyWalletEffect { readonly signTx: ( tx: Transaction.Transaction | string, - context?: { utxos?: ReadonlyArray } + context?: { utxos?: ReadonlyArray; referenceUtxos?: ReadonlyArray } ) => Effect.Effect readonly signMessage: ( address: CoreAddress.Address | RewardAddress.RewardAddress,