@@ -16,6 +16,18 @@ import util, { MIN_ADA_FOR_ONE_ASSET } from './utils';
1616import * as CardanoWasm from '@emurgo/cardano-serialization-lib-nodejs' ;
1717import { BigNum } from '@emurgo/cardano-serialization-lib-nodejs' ;
1818
19+ /**
20+ * Constants for transaction building
21+ */
22+ const FEE_COEFFICIENTS = {
23+ /** Linear fee parameter a */
24+ A_COEFFICIENT : '44' ,
25+ /** Linear fee parameter b */
26+ B_COEFFICIENT : '155381' ,
27+ /** Additional safety margin for the fee */
28+ SAFETY_MARGIN : '440' ,
29+ } ;
30+
1931export abstract class TransactionBuilder extends BaseTransactionBuilder {
2032 protected _transaction ! : Transaction ;
2133 protected _signers : KeyPair [ ] = [ ] ;
@@ -30,7 +42,18 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder {
3042 protected _withdrawals : Withdrawal [ ] = [ ] ;
3143 protected _type : TransactionType ;
3244 protected _multiAssets : Asset [ ] = [ ] ;
45+ /** Address of the transaction recipient */
46+ protected _recipientAddress : string ;
47+ /** Map of sender's assets by asset name */
48+ protected _senderAssetList : Record < string , any > = { } ;
3349 private _fee : BigNum ;
50+ /** Flag indicating if this is a token transaction */
51+ private _isTokenTransaction = false ;
52+ /** Deep clone of _senderAssetList - for manipulating during two iterations
53+ * - one for calculating the fee
54+ * - one for the actual transaction build with the calculated fee
55+ * */
56+ private _mutableSenderAssetList : Record < string , any > = { } ;
3457
3558 constructor ( _coinConfig : Readonly < CoinConfig > ) {
3659 super ( _coinConfig ) ;
@@ -58,17 +81,38 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder {
5881 return this ;
5982 }
6083
61- changeAddress ( addr : string , totalInputBalance : string ) : this {
84+ /**
85+ * Sets the change address and input balances for the transaction
86+ * @param addr - The address where change will be sent
87+ * @param totalInputBalance - The total ADA input balance in Lovelace
88+ * @param inputAssetList - Optional map of token assets in the input
89+ */
90+ changeAddress ( addr : string , totalInputBalance : string , inputAssetList ?: Record < string , any > ) : this {
6291 this . _changeAddress = addr ;
6392 this . _senderBalance = totalInputBalance ;
93+ this . _senderAssetList = inputAssetList ?? { } ;
94+ this . setMutableSenderAssetList ( ) ;
6495 return this ;
6596 }
6697
98+ setMutableSenderAssetList ( ) {
99+ this . _mutableSenderAssetList = JSON . parse ( JSON . stringify ( this . _senderAssetList ) ) ;
100+ }
101+
67102 fee ( fee : string ) : this {
68103 this . _fee = BigNum . from_str ( fee ) ;
69104 return this ;
70105 }
71106
107+ /**
108+ * Marks this transaction as a token transaction
109+ * Enables special handling for token transfers
110+ */
111+ isTokenTransaction ( ) {
112+ this . _isTokenTransaction = true ;
113+ return this ;
114+ }
115+
72116 /**
73117 * Initialize the transaction builder fields using the decoded transaction data
74118 *
@@ -132,8 +176,261 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder {
132176 return this . transaction ;
133177 }
134178
179+ /**
180+ * Builds a transaction that includes token assets
181+ * @returns The built transaction
182+ */
183+ private processTokenBuild ( ) : Transaction {
184+ const inputs = CardanoWasm . TransactionInputs . new ( ) ;
185+ let outputs = CardanoWasm . TransactionOutputs . new ( ) ;
186+ this . addInputs ( inputs ) ;
187+ // Fee is never set so far from IMS
188+ if ( this . _fee . is_zero ( ) ) {
189+ this . addOutputs ( outputs ) ;
190+ const txDraft = this . prepareAdaTransactionDraft ( inputs , outputs , false ) ;
191+ this . calculateFee ( txDraft ) ;
192+ this . setFeeInTransaction ( ) ;
193+ }
194+ // Reset outputs to add them back with the correct change
195+ outputs = CardanoWasm . TransactionOutputs . new ( ) ;
196+ this . setMutableSenderAssetList ( ) ;
197+ this . addOutputs ( outputs ) ;
198+ this . _transaction . transaction = this . prepareAdaTransactionDraft ( inputs , outputs , true ) ;
199+ return this . transaction ;
200+ }
201+
202+ /**
203+ * Adds transaction inputs to the transaction
204+ * @param inputs - The inputs collection to add to
205+ */
206+ private addInputs ( inputs ) {
207+ this . _transactionInputs . forEach ( ( input ) => {
208+ inputs . add (
209+ CardanoWasm . TransactionInput . new (
210+ CardanoWasm . TransactionHash . from_bytes ( Buffer . from ( input . transaction_id , 'hex' ) ) ,
211+ input . transaction_index
212+ )
213+ ) ;
214+ } ) ;
215+ }
216+
217+ /**
218+ * Adds outputs to the transaction including token outputs and change
219+ * @param outputs - The outputs collection to add to
220+ */
221+ private addOutputs ( outputs ) {
222+ const utxoBalance = CardanoWasm . BigNum . from_str ( this . _senderBalance ) ; // Total UTXO balance
223+ const change = utxoBalance . checked_sub ( this . _fee ) ;
224+ const changeAfterReceiverDeductions = this . addReceiverOutputs ( outputs , change ) ;
225+ this . addChangeOutput ( changeAfterReceiverDeductions , outputs ) ;
226+ }
227+
228+ /**
229+ * Adds receiver outputs to the transaction and calculates the remaining change
230+ * @param outputs - Transaction outputs collection
231+ * @param change - Current change amount in ADA
232+ * @returns Remaining change after adding receiver outputs
233+ */
234+ private addReceiverOutputs ( outputs , change ) {
235+ this . _transactionOutputs . forEach ( ( output ) => {
236+ if ( ! output . address ) {
237+ throw new BuildTransactionError ( 'Invalid output: missing address' ) ;
238+ }
239+
240+ const receiverAddress = output . address ;
241+ const receiverAmount = output . amount ;
242+
243+ try {
244+ const receiverAmountBN = CardanoWasm . BigNum . from_str ( receiverAmount ) ;
245+ change = change . checked_sub ( receiverAmountBN ) ;
246+
247+ if ( change . less_than ( CardanoWasm . BigNum . zero ( ) ) ) {
248+ throw new BuildTransactionError (
249+ 'Insufficient funds: not enough ADA to cover receiver output amounts and fees'
250+ ) ;
251+ }
252+
253+ const multiAssets = output . multiAssets as Asset ;
254+ if ( multiAssets ) {
255+ const policyId = multiAssets . policy_id ;
256+ const assetName = multiAssets . asset_name ;
257+ const quantity = multiAssets . quantity ;
258+ const fingerprint = multiAssets . fingerprint as string ;
259+
260+ const currentQty = this . _mutableSenderAssetList [ fingerprint ] . quantity ;
261+ const remainingQty = BigInt ( currentQty ) - BigInt ( quantity ) ;
262+ this . _mutableSenderAssetList [ fingerprint ] . quantity = ( remainingQty > 0n ? remainingQty : 0n ) . toString ( ) ;
263+
264+ if ( CardanoWasm . BigNum . from_str ( this . _mutableSenderAssetList [ fingerprint ] . quantity ) . is_zero ( ) ) {
265+ throw new BuildTransactionError ( 'Insufficient qty: not enough token qty to cover receiver output' ) ;
266+ }
267+
268+ const minAmountNeededForAssetOutput = this . addTokensToOutput ( change , outputs , receiverAddress , {
269+ policy_id : policyId ,
270+ asset_name : assetName ,
271+ quantity,
272+ fingerprint,
273+ } ) ;
274+
275+ change = change . checked_sub ( minAmountNeededForAssetOutput ) ;
276+ }
277+ } catch ( e ) {
278+ if ( e instanceof BuildTransactionError ) {
279+ throw e ;
280+ }
281+ throw new BuildTransactionError ( `Error processing output: ${ e . message } ` ) ;
282+ }
283+ } ) ;
284+ return change ;
285+ }
286+
287+ /**
288+ * Adds tokens to a transaction output
289+ * @param change - The current change amount in Lovelace
290+ * @param outputs - The outputs collection to add to
291+ * @param address - The recipient address
292+ * @param asset - The asset to add
293+ * @returns The minimum ADA amount needed for this asset output
294+ */
295+ private addTokensToOutput ( change : BigNum , outputs : CardanoWasm . TransactionOutputs , address , asset : Asset ) : BigNum {
296+ const minAmountNeededForOneAssetOutput = CardanoWasm . BigNum . from_str ( MIN_ADA_FOR_ONE_ASSET ) ;
297+ if ( change . less_than ( minAmountNeededForOneAssetOutput ) ) {
298+ throw new BuildTransactionError (
299+ `Insufficient funds: need a minimum of ${ MIN_ADA_FOR_ONE_ASSET } lovelace per output to construct token transactions`
300+ ) ;
301+ }
302+ this . buildTokens ( asset , minAmountNeededForOneAssetOutput , outputs , address ) ;
303+ return minAmountNeededForOneAssetOutput ;
304+ }
305+
306+ /**
307+ * Builds token outputs for a transaction
308+ * @param asset - The asset to include in the output
309+ * @param minAmountNeededForOneAssetOutput - Minimum ADA required for this asset output
310+ * @param outputs - Transaction outputs collection
311+ * @param address - Recipient address
312+ */
313+ private buildTokens ( asset : Asset , minAmountNeededForOneAssetOutput , outputs , address ) {
314+ let txOutputBuilder = CardanoWasm . TransactionOutputBuilder . new ( ) ;
315+ const toAddress = util . getWalletAddress ( address ) ;
316+ txOutputBuilder = txOutputBuilder . with_address ( toAddress ) ;
317+ let txOutputAmountBuilder = txOutputBuilder . next ( ) ;
318+ const assetName = CardanoWasm . AssetName . new ( Buffer . from ( asset . asset_name , 'hex' ) ) ;
319+ const policyId = CardanoWasm . ScriptHash . from_bytes ( Buffer . from ( asset . policy_id , 'hex' ) ) ;
320+ const multiAsset = CardanoWasm . MultiAsset . new ( ) ;
321+ const assets = CardanoWasm . Assets . new ( ) ;
322+ assets . insert ( assetName , CardanoWasm . BigNum . from_str ( asset . quantity ) ) ;
323+ multiAsset . insert ( policyId , assets ) ;
324+
325+ txOutputAmountBuilder = txOutputAmountBuilder . with_coin_and_asset ( minAmountNeededForOneAssetOutput , multiAsset ) ;
326+
327+ const txOutput = txOutputAmountBuilder . build ( ) ;
328+ outputs . add ( txOutput ) ;
329+ }
330+
331+ /**
332+ * Adds a change output to the transaction
333+ * @param change - The change amount in Lovelace
334+ * @param outputs - The outputs collection to add to
335+ */
336+ private addChangeOutput ( change , outputs ) {
337+ const changeAddress = util . getWalletAddress ( this . _changeAddress ) ;
338+ Object . keys ( this . _mutableSenderAssetList ) . forEach ( ( fingerprint ) => {
339+ const asset = this . _mutableSenderAssetList [ fingerprint ] ;
340+ const changeQty = asset . quantity ;
341+ const policyId = asset . policy_id ;
342+ const assetName = asset . asset_name ;
343+
344+ if ( CardanoWasm . BigNum . from_str ( changeQty ) . is_zero ( ) ) {
345+ return ;
346+ }
347+
348+ const minAmountNeededForAssetOutput = this . addTokensToOutput ( change , outputs , this . _changeAddress , {
349+ policy_id : policyId ,
350+ asset_name : assetName ,
351+ quantity : changeQty ,
352+ fingerprint,
353+ } ) ;
354+ change = change . checked_sub ( minAmountNeededForAssetOutput ) ;
355+ } ) ;
356+ if ( ! change . is_zero ( ) ) {
357+ const changeOutput = CardanoWasm . TransactionOutput . new ( changeAddress , CardanoWasm . Value . new ( change ) ) ;
358+ outputs . add ( changeOutput ) ;
359+ }
360+ }
361+
362+ /**
363+ * Sets the calculated fee in the transaction
364+ */
365+ private setFeeInTransaction ( ) {
366+ this . _transaction . fee ( this . _fee . to_str ( ) ) ;
367+ }
368+
369+ /**
370+ * Calculates the transaction fee based on transaction size
371+ * @param txDraft - Draft transaction to calculate fee for
372+ */
373+ private calculateFee ( txDraft ) {
374+ const linearFee = CardanoWasm . LinearFee . new (
375+ CardanoWasm . BigNum . from_str ( FEE_COEFFICIENTS . A_COEFFICIENT ) ,
376+ CardanoWasm . BigNum . from_str ( FEE_COEFFICIENTS . B_COEFFICIENT )
377+ ) ;
378+
379+ // Calculate the fee based off our dummy transaction
380+ const fee = CardanoWasm . min_fee ( txDraft , linearFee ) . checked_add ( BigNum . from_str ( FEE_COEFFICIENTS . SAFETY_MARGIN ) ) ;
381+ this . _fee = fee ;
382+ }
383+
384+ private prepareAdaTransactionDraft ( inputs , outputs , refreshSignatures = false ) {
385+ const txBody = CardanoWasm . TransactionBody . new_tx_body ( inputs , outputs , this . _fee ) ;
386+ txBody . set_ttl ( CardanoWasm . BigNum . from_str ( this . _ttl . toString ( ) ) ) ;
387+ const txHash = CardanoWasm . hash_transaction ( txBody ) ;
388+
389+ // we add witnesses once so that we can get the appropriate amount of signers for calculating the fee
390+ const witnessSet = CardanoWasm . TransactionWitnessSet . new ( ) ;
391+ const vkeyWitnesses = CardanoWasm . Vkeywitnesses . new ( ) ;
392+ this . _signers . forEach ( ( keyPair ) => {
393+ const prv = keyPair . getKeys ( ) . prv as string ;
394+ const vkeyWitness = CardanoWasm . make_vkey_witness (
395+ txHash ,
396+ CardanoWasm . PrivateKey . from_normal_bytes ( Buffer . from ( prv , 'hex' ) )
397+ ) ;
398+ vkeyWitnesses . add ( vkeyWitness ) ;
399+ } ) ;
400+ if ( refreshSignatures ) {
401+ this . _transaction . signature . length = 0 ;
402+ }
403+ this . getAllSignatures ( ) . forEach ( ( signature ) => {
404+ const vkey = CardanoWasm . Vkey . new ( CardanoWasm . PublicKey . from_bytes ( Buffer . from ( signature . publicKey . pub , 'hex' ) ) ) ;
405+ const ed255Sig = CardanoWasm . Ed25519Signature . from_bytes ( signature . signature ) ;
406+ vkeyWitnesses . add ( CardanoWasm . Vkeywitness . new ( vkey , ed255Sig ) ) ;
407+ } ) ;
408+ if ( vkeyWitnesses . len ( ) === 0 ) {
409+ const prv = CardanoWasm . PrivateKey . generate_ed25519 ( ) ;
410+ const vkeyWitness = CardanoWasm . make_vkey_witness ( txHash , prv ) ;
411+ vkeyWitnesses . add ( vkeyWitness ) ;
412+ if ( this . _type !== TransactionType . Send ) {
413+ vkeyWitnesses . add ( vkeyWitness ) ;
414+ }
415+ }
416+ witnessSet . set_vkeys ( vkeyWitnesses ) ;
417+
418+ // add in certificates to get mock size
419+ const draftCerts = CardanoWasm . Certificates . new ( ) ;
420+ for ( const cert of this . _certs ) {
421+ draftCerts . add ( cert ) ;
422+ }
423+ txBody . set_certs ( draftCerts ) ;
424+
425+ const txDraft = CardanoWasm . Transaction . new ( txBody , witnessSet ) ;
426+ return txDraft ;
427+ }
428+
135429 /** @inheritdoc */
136430 protected async buildImplementation ( ) : Promise < Transaction > {
431+ if ( this . _isTokenTransaction ) {
432+ return this . processTokenBuild ( ) ;
433+ }
137434 const inputs = CardanoWasm . TransactionInputs . new ( ) ;
138435 this . _transactionInputs . forEach ( ( input ) => {
139436 inputs . add (
@@ -291,9 +588,10 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder {
291588 // reset the outputs collection because now our last output has changed
292589 outputs = CardanoWasm . TransactionOutputs . new ( ) ;
293590 this . _transactionOutputs . forEach ( ( output ) => {
294- if ( output . multiAssets ) {
295- const policyId = output . multiAssets . keys ( ) . get ( 0 ) ;
296- const assets = output . multiAssets . get ( policyId ) ;
591+ const multiAssets = output . multiAssets as CardanoWasm . MultiAsset ;
592+ if ( multiAssets ) {
593+ const policyId = multiAssets . keys ( ) . get ( 0 ) ;
594+ const assets = multiAssets . get ( policyId ) ;
297595 const assetName = assets ! . keys ( ) . get ( 0 ) ;
298596 const quantity = assets ! . get ( assetName ) ;
299597 let txOutputBuilder = CardanoWasm . TransactionOutputBuilder . new ( ) ;
0 commit comments