@@ -10,7 +10,15 @@ import {
1010 TransactionType ,
1111 UtilsError ,
1212} from '@bitgo/sdk-core' ;
13- import { Asset , Transaction , TransactionInput , TransactionOutput , Withdrawal , SponsorshipInfo } from './transaction' ;
13+ import {
14+ Asset ,
15+ ExplicitOutput ,
16+ SponsorshipInfo ,
17+ Transaction ,
18+ TransactionInput ,
19+ TransactionOutput ,
20+ Withdrawal ,
21+ } from './transaction' ;
1422import { KeyPair } from './keyPair' ;
1523import util , { MIN_ADA_FOR_ONE_ASSET } from './utils' ;
1624import * as CardanoWasm from '@emurgo/cardano-serialization-lib-nodejs' ;
@@ -50,6 +58,12 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder {
5058 private _fee : BigNum ;
5159 /** Flag indicating if this is a token transaction */
5260 private _isTokenTransaction = false ;
61+ /**
62+ * Pre-computed outputs for single-asset consolidation / explicit-output builds.
63+ * When non-empty, build() uses processExplicitOutputsBuild: the SDK only
64+ * calculates fee and appends the fee-address change output (requires sponsorshipInfo).
65+ */
66+ private _explicitOutputs : ExplicitOutput [ ] = [ ] ;
5367 /** Deep clone of _senderAssetList - for manipulating during two iterations
5468 * - one for calculating the fee
5569 * - one for the actual transaction build with the calculated fee
@@ -119,6 +133,17 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder {
119133 return this ;
120134 }
121135
136+ /**
137+ * Adds a pre-computed output for single-asset consolidation / explicit-output builds.
138+ * When at least one explicit output is present, build() uses the explicit-output path
139+ * (fee + fee-address change only); requires sponsorshipInfo.
140+ * Uses plain Asset objects so callers do not need to import CardanoWasm.
141+ */
142+ explicitOutput ( o : ExplicitOutput ) : this {
143+ this . _explicitOutputs . push ( o ) ;
144+ return this ;
145+ }
146+
122147 /**
123148 * Initialize the transaction builder fields using the decoded transaction data
124149 *
@@ -474,8 +499,88 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder {
474499 return txDraft ;
475500 }
476501
502+ /**
503+ * Converts a plain ExplicitOutput into a Cardano TransactionOutput and
504+ * adds it to `outputs`. Supports multiple policy IDs / asset names.
505+ */
506+ private addExplicitOutput ( output : ExplicitOutput , outputs : CardanoWasm . TransactionOutputs ) : void {
507+ const amount = CardanoWasm . BigNum . from_str ( output . amount ) ;
508+ const addr = util . getWalletAddress ( output . address ) ;
509+ if ( output . assets && output . assets . length > 0 ) {
510+ const multiAsset = CardanoWasm . MultiAsset . new ( ) ;
511+ for ( const asset of output . assets ) {
512+ const policyId = CardanoWasm . ScriptHash . from_bytes ( Buffer . from ( asset . policy_id , 'hex' ) ) ;
513+ const assetName = CardanoWasm . AssetName . new ( Buffer . from ( asset . asset_name , 'hex' ) ) ;
514+ const qty = CardanoWasm . BigNum . from_str ( asset . quantity ) ;
515+ let existingAssets = multiAsset . get ( policyId ) ;
516+ if ( ! existingAssets ) {
517+ existingAssets = CardanoWasm . Assets . new ( ) ;
518+ }
519+ existingAssets . insert ( assetName , qty ) ;
520+ multiAsset . insert ( policyId , existingAssets ) ;
521+ }
522+ const txOutputBuilder = CardanoWasm . TransactionOutputBuilder . new ( ) . with_address ( addr ) ;
523+ let txOutputAmountBuilder = txOutputBuilder . next ( ) ;
524+ txOutputAmountBuilder = txOutputAmountBuilder . with_coin_and_asset ( amount , multiAsset ) ;
525+ outputs . add ( txOutputAmountBuilder . build ( ) ) ;
526+ } else {
527+ outputs . add ( CardanoWasm . TransactionOutput . new ( addr , CardanoWasm . Value . new ( amount ) ) ) ;
528+ }
529+ }
530+
531+ /**
532+ * Builds a Cardano outputs collection from _explicitOutputs and appends
533+ * the fee-address change output based on the supplied fee amount.
534+ * Used exclusively by processExplicitOutputsBuild.
535+ */
536+ private buildExplicitOutputsCollection ( fee : BigNum ) : CardanoWasm . TransactionOutputs {
537+ if ( ! this . _sponsorshipInfo ) {
538+ throw new BuildTransactionError ( 'explicit outputs build requires sponsorshipInfo to be set' ) ;
539+ }
540+ const outputs = CardanoWasm . TransactionOutputs . new ( ) ;
541+ for ( const output of this . _explicitOutputs ) {
542+ this . addExplicitOutput ( output , outputs ) ;
543+ }
544+ const feeBalance = CardanoWasm . BigNum . from_str ( this . _sponsorshipInfo . feeAddressInputBalance ) ;
545+ const feeAddressChange = feeBalance . checked_sub ( fee ) ;
546+ if ( ! feeAddressChange . is_zero ( ) ) {
547+ const feeAddr = util . getWalletAddress ( this . _sponsorshipInfo . feeAddress ) ;
548+ outputs . add ( CardanoWasm . TransactionOutput . new ( feeAddr , CardanoWasm . Value . new ( feeAddressChange ) ) ) ;
549+ }
550+ return outputs ;
551+ }
552+
553+ /**
554+ * Build path for single-asset consolidation and token withdrawal builds
555+ *
556+ * Two-pass approach mirrors processTokenBuild:
557+ * 1. Draft with zero fee → calculate actual fee.
558+ * 2. Rebuild outputs with actual fee deducted from fee-address balance.
559+ */
560+ private processExplicitOutputsBuild ( ) : Transaction {
561+ if ( this . _explicitOutputs . length === 0 ) {
562+ throw new BuildTransactionError ( 'explicit outputs build requires at least one explicitOutput' ) ;
563+ }
564+ const inputs = CardanoWasm . TransactionInputs . new ( ) ;
565+ this . addInputs ( inputs ) ;
566+
567+ if ( this . _fee . is_zero ( ) ) {
568+ const draftOutputs = this . buildExplicitOutputsCollection ( BigNum . zero ( ) ) ;
569+ const txDraft = this . prepareAdaTransactionDraft ( inputs , draftOutputs , false ) ;
570+ this . calculateFee ( txDraft ) ;
571+ this . setFeeInTransaction ( ) ;
572+ }
573+
574+ const finalOutputs = this . buildExplicitOutputsCollection ( this . _fee ) ;
575+ this . _transaction . transaction = this . prepareAdaTransactionDraft ( inputs , finalOutputs , true ) ;
576+ return this . transaction ;
577+ }
578+
477579 /** @inheritdoc */
478580 protected async buildImplementation ( ) : Promise < Transaction > {
581+ if ( this . _explicitOutputs . length > 0 ) {
582+ return this . processExplicitOutputsBuild ( ) ;
583+ }
479584 /**
480585 * Fee address utxo reservation builds a new transaction that goes through legacy build
481586 * rebuild flag is just a hack to redirect the flow to the legacy build
0 commit comments