@@ -57,6 +57,93 @@ const distributeAssets = (
57
57
return adjustedOutputs ;
58
58
} ;
59
59
60
+ /**
61
+ * Computes the lowest amount a change output is allowed to have during change
62
+ * distribution within the same stake address.
63
+ *
64
+ * @param amount The total available lovelace amount.
65
+ */
66
+ const getMinUtxoAmount = ( amount : bigint ) : bigint => {
67
+ // The smaller the value, the bigger the amount of UTXOs that will be present in the stake address.
68
+ const granularityFactor = new BigNumber ( 0.03 ) ;
69
+
70
+ // Computes the smallest number with the same number of tens, for example: 3_725_000, will yield 1_000_000.
71
+ let tens = amount . toString ( ) . length - 1 ;
72
+ let minLovelaceStr = '1' ;
73
+
74
+ while ( tens > 0 ) {
75
+ minLovelaceStr += '0' ;
76
+ -- tens ;
77
+ }
78
+
79
+ return BigInt ( new BigNumber ( minLovelaceStr ) . multipliedBy ( granularityFactor ) . toFixed ( 0 , 0 ) ) ;
80
+ } ;
81
+
82
+ /**
83
+ * Given a change output, split it following an exponential distribution. For example
84
+ * 100000 will yield:
85
+ *
86
+ * [ 50000n, 25000n, 12500n, 6250n, 3125n, 3125n ]
87
+ *
88
+ * We chose an exponential distribution (n**2), because it gives the best compromise in granularity
89
+ * and amount of UTXOs generated. We don't want too many UTXOs as this could make the transaction exceed the max allow
90
+ * TX size, but at the same time we want a diverse and big enough amount of UTXOs to reasonably build any TX with a fairly
91
+ * small amount of inputs.
92
+ *
93
+ * Using an exponential distribution will also guarantee that the number of UTXOs generated will be more or less the same
94
+ * regardless of the amount of total available lovelace, for example:
95
+ *
96
+ * 10 => [ 5n, 2n, 1n, 1n, 1n ]
97
+ * 100 => [ 50n, 25n, 12n, 6n, 3n, 4n ]
98
+ * 1000 => [ 500n, 250n, 125n, 62n, 31n, 32n ]
99
+ * 10000 => [ 5000n, 2500n, 1250n, 625n, 312n, 313n ]
100
+ * 100000 => [ 50000n, 25000n, 12500n, 6250n, 3125n, 3125n ]
101
+ * 1000000 => [ 500000n, 250000n, 125000n, 62500n, 31250n, 31250n ]
102
+ *
103
+ * @param output The output to be split.
104
+ * @param computeMinimumCoinQuantity ComputeMinimumCoinQuantity.
105
+ */
106
+ const splitChangeOutput = (
107
+ output : Cardano . TxOut ,
108
+ computeMinimumCoinQuantity : ComputeMinimumCoinQuantity
109
+ ) : Array < Cardano . TxOut > => {
110
+ const amount = output . value . coins ;
111
+ const minUtxoAdaAmount = getMinUtxoAmount ( amount ) ;
112
+
113
+ let remaining = amount ;
114
+ let runningAmount = 0n ;
115
+ const amounts = new Array < bigint > ( ) ;
116
+ const divisor = new BigNumber ( 2 ) ;
117
+
118
+ while ( remaining > minUtxoAdaAmount ) {
119
+ const val = BigInt ( new BigNumber ( remaining . toString ( ) ) . dividedBy ( divisor ) . toFixed ( 0 , 0 ) ) ;
120
+
121
+ const updatedRemaining = remaining - val ;
122
+ if (
123
+ updatedRemaining <= minUtxoAdaAmount ||
124
+ updatedRemaining <=
125
+ computeMinimumCoinQuantity ( {
126
+ address : output . address ,
127
+ value : { assets : output . value . assets , coins : amount - runningAmount }
128
+ } )
129
+ ) {
130
+ amounts . push ( amount - runningAmount ) ; // Add all that remains to account for rounding errors
131
+ break ;
132
+ }
133
+
134
+ runningAmount += val ;
135
+
136
+ amounts . push ( val ) ;
137
+
138
+ remaining -= val ;
139
+ }
140
+
141
+ return amounts . map ( ( coins ) => ( {
142
+ address : output . address ,
143
+ value : { assets : output . value . assets , coins }
144
+ } ) ) ;
145
+ } ;
146
+
60
147
/**
61
148
* Splits the change proportionally between the given addresses. This algorithm makes
62
149
* the best effort to be as accurate as possible in distributing the amounts, however, due to rounding
@@ -120,7 +207,8 @@ export const splitChange = async (
120
207
changeOutputs [ changeOutputs . length - 1 ] . value . coins += missingAllocation ;
121
208
}
122
209
123
- const sortedOutputs = changeOutputs . sort ( sortByCoins ) . filter ( ( out ) => out . value . coins > 0n ) ;
210
+ const splitOutputs = changeOutputs . flatMap ( ( output ) => splitChangeOutput ( output , computeMinimumCoinQuantity ) ) ;
211
+ const sortedOutputs = splitOutputs . sort ( sortByCoins ) . filter ( ( out ) => out . value . coins > 0n ) ;
124
212
125
213
if ( sortedOutputs && sortedOutputs . length > 0 ) sortedOutputs [ 0 ] . value . assets = totalChangeAssets ; // Add all assets to the 'biggest' output.
126
214
0 commit comments