@@ -247,20 +247,6 @@ export function selectUtxosForAmounts(
247247 const primaryTokenIdStr =
248248 typeof primaryTokenId === 'string' ? primaryTokenId : primaryTokenId
249249
250- const requiredAda =
251- ( Object . keys ( requiredAmounts ) as Array < TokenId > ) . reduce ( ( sum , tokenId ) => {
252- const tokenIdStr = typeof tokenId === 'string' ? tokenId : tokenId
253- if ( tokenIdStr === primaryTokenIdStr ) {
254- const quantity = requiredAmounts [ tokenId ]
255- const qtyStr = typeof quantity === 'string' ? quantity : quantity
256- return sum + BigInt ( qtyStr || '0' )
257- }
258- return sum
259- } , BigInt ( 0 ) ) +
260- BigInt ( feeStr ) +
261- minUtxoValue +
262- feeBuffer
263-
264250 // Get all required token IDs (excluding primary token)
265251 const requiredTokenIds = new Set (
266252 Object . keys ( requiredAmounts ) . filter ( ( id ) => {
@@ -299,7 +285,6 @@ export function selectUtxosForAmounts(
299285 }
300286
301287 // Check if we have enough of each token
302- let needsMoreAda = selectedAda < requiredAda
303288 const needsMoreTokens : TokenId [ ] = [ ]
304289
305290 for ( const tokenId of requiredTokenIds ) {
@@ -331,11 +316,105 @@ export function selectUtxosForAmounts(
331316 return selected
332317 }
333318
319+ // Calculate tokens that will remain in change (tokens in selected UTXOs minus tokens being sent)
320+ const tokensInChange : Record < string , bigint > = { }
321+ for ( const [ tokenId , inputAmount ] of Object . entries ( selectedAmounts ) ) {
322+ if ( tokenId === primaryTokenIdStr ) continue
323+ const outputAmount = BigInt ( requiredAmounts [ tokenId as TokenId ] || '0' )
324+ const remaining = inputAmount - outputAmount
325+ if ( remaining > BigInt ( 0 ) ) {
326+ tokensInChange [ tokenId ] = remaining
327+ }
328+ }
329+
330+ // Estimate minimum UTXO for change output based on token count
331+ // When tokens will be in change, the minimum UTXO requirement increases significantly
332+ // Use a conservative estimate: base minimum + additional ADA per token
333+ // This is a heuristic since exact calculation requires CSL
334+ const tokenCountInChange = Object . keys ( tokensInChange ) . length
335+ let estimatedMinUtxoForChange = minUtxoValue
336+ if ( tokenCountInChange > 0 ) {
337+ // Conservative estimate: base minimum + 0.1 ADA per token (scaled for safety)
338+ // Actual minimum can be higher depending on token sizes, but this provides a buffer
339+ const adaPerToken = BigInt ( '100000' ) // 0.1 ADA per token
340+ const tokenMultiplier = BigInt ( Math . max ( tokenCountInChange , 1 ) )
341+ estimatedMinUtxoForChange =
342+ minUtxoValue + adaPerToken * tokenMultiplier * BigInt ( 2 ) // 2x multiplier for safety
343+ }
344+
345+ // Calculate base required ADA (outputs + fee + buffer) - min UTXO calculated separately
346+ const baseRequiredAda =
347+ ( Object . keys ( requiredAmounts ) as Array < TokenId > ) . reduce ( ( sum , tokenId ) => {
348+ const tokenIdStr = typeof tokenId === 'string' ? tokenId : tokenId
349+ if ( tokenIdStr === primaryTokenIdStr ) {
350+ const quantity = requiredAmounts [ tokenId ]
351+ const qtyStr = typeof quantity === 'string' ? quantity : quantity
352+ return sum + BigInt ( qtyStr || '0' )
353+ }
354+ return sum
355+ } , BigInt ( 0 ) ) +
356+ BigInt ( feeStr ) +
357+ feeBuffer
358+
359+ // Calculate required ADA including proper change output minimum
360+ let requiredAda = baseRequiredAda + estimatedMinUtxoForChange
361+
334362 // If we need more ADA, select from remaining UTXOs
363+ // Prefer pure ADA UTXOs to avoid adding unexpected tokens to change output
364+ // This prevents underestimating minimum UTXO when non-required tokens end up in change
365+ let needsMoreAda = selectedAda < requiredAda
335366 if ( needsMoreAda ) {
336- const sortedRemaining = sortUtxosByAda ( utxosWithoutTokens , primaryTokenId )
367+ const pureAdaUtxos = filterPureAdaUtxos ( utxosWithoutTokens , primaryTokenId )
368+ const otherUtxos = utxosWithoutTokens . filter (
369+ ( u ) => ! pureAdaUtxos . includes ( u ) ,
370+ )
371+
372+ // Always prefer pure ADA UTXOs first to avoid adding tokens to change
373+ // This ensures minimum UTXO estimate remains accurate
374+ const sortedRemaining = [
375+ ...sortUtxosByAda ( pureAdaUtxos , primaryTokenId ) ,
376+ ...sortUtxosByAda ( otherUtxos , primaryTokenId ) ,
377+ ]
378+
379+ // Track tokens being added and update minimum UTXO estimate dynamically
380+ let currentTokensInChange = { ...tokensInChange }
381+ let currentEstimatedMinUtxo = estimatedMinUtxoForChange
382+ const adaPerToken = BigInt ( '100000' ) // 0.1 ADA per token
337383
338384 for ( const utxo of sortedRemaining ) {
385+ // Check if this UTXO adds any tokens to change
386+ const utxoTokenIds = ( Object . keys ( utxo . balance ) as Array < TokenId > ) . filter (
387+ ( id ) => {
388+ const idStr = typeof id === 'string' ? id : id
389+ return idStr !== primaryTokenIdStr
390+ } ,
391+ )
392+
393+ // Update tokens in change if UTXO contains non-required tokens
394+ if ( utxoTokenIds . length > 0 ) {
395+ for ( const tokenId of utxoTokenIds ) {
396+ const tokenIdStr = typeof tokenId === 'string' ? tokenId : tokenId
397+ const tokenAmount = BigInt ( utxo . balance [ tokenId ] || '0' )
398+ if ( tokenAmount > BigInt ( 0 ) ) {
399+ // This token will be added to change (not required, so goes to change)
400+ if ( ! currentTokensInChange [ tokenIdStr ] ) {
401+ currentTokensInChange [ tokenIdStr ] = BigInt ( 0 )
402+ }
403+ currentTokensInChange [ tokenIdStr ] += tokenAmount
404+ }
405+ }
406+
407+ // Recalculate minimum UTXO estimate based on updated token count
408+ const newTokenCount = Object . keys ( currentTokensInChange ) . length
409+ if ( newTokenCount > 0 ) {
410+ const tokenMultiplier = BigInt ( newTokenCount )
411+ currentEstimatedMinUtxo =
412+ minUtxoValue + adaPerToken * tokenMultiplier * BigInt ( 2 )
413+ // Update required ADA with new minimum UTXO estimate
414+ requiredAda = baseRequiredAda + currentEstimatedMinUtxo
415+ }
416+ }
417+
339418 if ( selectedAda >= requiredAda ) break
340419 selected . push ( utxo )
341420 selectedAda += BigInt ( utxo . balance [ primaryTokenIdStr ] || '0' )
0 commit comments