Skip to content

Commit b3da97f

Browse files
feat(input-selection): greedy input selection now generates more than one change output per stake id
1 parent 7ee2628 commit b3da97f

File tree

5 files changed

+380
-117
lines changed

5 files changed

+380
-117
lines changed

Diff for: packages/input-selection/src/GreedySelection/util.ts

+89-1
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,93 @@ const distributeAssets = (
5757
return adjustedOutputs;
5858
};
5959

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+
60147
/**
61148
* Splits the change proportionally between the given addresses. This algorithm makes
62149
* 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 (
120207
changeOutputs[changeOutputs.length - 1].value.coins += missingAllocation;
121208
}
122209

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);
124212

125213
if (sortedOutputs && sortedOutputs.length > 0) sortedOutputs[0].value.assets = totalChangeAssets; // Add all assets to the 'biggest' output.
126214

0 commit comments

Comments
 (0)