Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(input-selection): added new greedy input selector
- Loading branch information
1 parent
7255fb2
commit 11512e9
Showing
24 changed files
with
1,649 additions
and
203 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
149 changes: 149 additions & 0 deletions
149
packages/input-selection/src/GreedySelection/GreedyInputSelector.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,149 @@ | ||
import { AssetId } from '@cardano-sdk/core/dist/cjs/Cardano'; | ||
import { Cardano } from '@cardano-sdk/core'; | ||
import { InputSelectionError, InputSelectionFailure } from '../InputSelectionError'; | ||
import { InputSelectionParameters, InputSelector, SelectionConstraints, SelectionResult } from '../types'; | ||
import { | ||
addTokenMaps, | ||
getAssetQuantities, | ||
getCoinQuantity, | ||
hasNegativeAssetValue, | ||
sortByCoins, | ||
splitChange, | ||
subtractTokenMaps, | ||
toValues | ||
} from '../util'; | ||
|
||
/** | ||
* Greedy selection initialization properties. | ||
*/ | ||
export interface GreedySelectorProps { | ||
/** | ||
* Callback that returns a map of addresses with their intended proportions. This selector | ||
* will create N change outputs at this change addresses with the given proportions. | ||
*/ | ||
getChangeAddresses: () => Promise<Map<Cardano.PaymentAddress, number>>; | ||
} | ||
|
||
/** | ||
* Given a set of input and outputs, compute the fee. Then | ||
* extract the fee from the output with the highest value. Do this recursively until we | ||
* create a valid transaction. | ||
* | ||
* @param constraints The selection constraints. | ||
* @param inputs The inputs of the transaction. | ||
* @param outputs The outputs of the transaction. | ||
* @param changeOutputs The list of change outputs. | ||
* @param currentFee The current computed fee for this selection. | ||
*/ | ||
const adjustOutputsForFee = async ( | ||
constraints: SelectionConstraints, | ||
inputs: Set<Cardano.Utxo>, | ||
outputs: Set<Cardano.TxOut>, | ||
changeOutputs: Array<Cardano.TxOut>, | ||
currentFee: bigint | ||
): Promise<{ fee: bigint; change: Array<Cardano.TxOut> }> => { | ||
let result = { change: [...changeOutputs], fee: currentFee }; | ||
|
||
// Compute the fee. If the current fee is less than the newly computed fee, deduct the difference from the change output | ||
// with the highest coin value and try again. If the coin value falls below the minimum coin quantity, deduct from the | ||
// next highest coin value output instead. If we run out of outputs to deduct the fee from, throw. | ||
const totalOutputs = new Set([...outputs, ...changeOutputs]); | ||
const fee = await constraints.computeMinimumCost({ | ||
change: [], | ||
fee: currentFee, | ||
inputs, | ||
outputs: totalOutputs | ||
}); | ||
|
||
if (currentFee < fee) { | ||
const feeDifference = fee - currentFee; | ||
const updatedOutputs = [...changeOutputs]; | ||
|
||
updatedOutputs.sort(sortByCoins); | ||
|
||
let feeAccountedFor = false; | ||
for (const output of updatedOutputs) { | ||
const adjustedCoins = output.value.coins - feeDifference; | ||
|
||
if (adjustedCoins > constraints.computeMinimumCoinQuantity(output)) { | ||
output.value.coins = adjustedCoins; | ||
feeAccountedFor = true; | ||
break; | ||
} | ||
} | ||
|
||
if (!feeAccountedFor) throw new InputSelectionError(InputSelectionFailure.UtxoBalanceInsufficient); | ||
|
||
result = await adjustOutputsForFee(constraints, inputs, outputs, updatedOutputs, fee); | ||
} | ||
|
||
return result; | ||
}; | ||
|
||
/** | ||
* Selects all UTXOs to fulfill the amount required for the given outputs and return the remaining balance (if any) | ||
* as change in a single output. | ||
* | ||
* In its current form, this input selection strategy is not suitable for normal transactions. | ||
* | ||
* Here are a few caveats: | ||
* | ||
* - This input selector will slightly alter the given outputs to account for the transaction fee if the wallet tries | ||
* to spend the whole balance. | ||
* - This is a very naive selection algorithm and the resulting selection could be invalid if the user has too many UTXOs or if | ||
* the user has too many native assets. | ||
*/ | ||
export class GreedyInputSelector implements InputSelector { | ||
#props: GreedySelectorProps; | ||
|
||
constructor(props: GreedySelectorProps) { | ||
this.#props = props; | ||
} | ||
|
||
async select(params: InputSelectionParameters): Promise<SelectionResult> { | ||
const { utxo: inputs, outputs, constraints, implicitValue } = params; | ||
const utxoValues = toValues([...inputs]); | ||
const outputsValues = toValues([...outputs]); | ||
const totalLovelaceInUtxoSet = getCoinQuantity(utxoValues); | ||
const totalLovelaceInOutputSet = getCoinQuantity(outputsValues); | ||
const totalAssetsInUtxoSet = getAssetQuantities(utxoValues); | ||
const totalAssetsInOutputSet = getAssetQuantities(outputsValues); | ||
const implicitCoinInput = implicitValue?.coin?.input || 0n; | ||
const implicitCoinOutput = implicitValue?.coin?.deposit || 0n; | ||
const implicitAssetInput = implicitValue?.mint || new Map<AssetId, bigint>(); | ||
const totalLovelaceInput = totalLovelaceInUtxoSet + implicitCoinInput; | ||
const totalLovelaceOutput = totalLovelaceInOutputSet + implicitCoinOutput; | ||
const totalAssetsInput = addTokenMaps(totalAssetsInUtxoSet, implicitAssetInput); | ||
|
||
// Calculate the difference between the given outputs and the UTXO set. This will let us know | ||
// how much we need to return as change. | ||
const changeLovelace = totalLovelaceInput - totalLovelaceOutput; | ||
const changeAssets = subtractTokenMaps(totalAssetsInput, totalAssetsInOutputSet); | ||
|
||
// If the wallet tries to spend more than we can cover with the current UTXO set + implicit value throw. | ||
if (changeLovelace <= 0n || hasNegativeAssetValue(changeAssets)) | ||
throw new InputSelectionError(InputSelectionFailure.UtxoBalanceInsufficient); | ||
|
||
// Split the remaining amount to be returned as change proportionally between the given addresses. | ||
const changeOutputs = await splitChange( | ||
this.#props.getChangeAddresses, | ||
changeLovelace, | ||
changeAssets, | ||
constraints.computeMinimumCoinQuantity, | ||
constraints.tokenBundleSizeExceedsLimit | ||
); | ||
|
||
// Adjust the change outputs to account for the transaction fee. | ||
const adjustedChangeOutputs = await adjustOutputsForFee(constraints, inputs, outputs, changeOutputs, 0n); | ||
|
||
return { | ||
remainingUTxO: new Set<Cardano.Utxo>(), // This input selection always consumes all inputs. | ||
selection: { | ||
change: adjustedChangeOutputs.change, | ||
fee: adjustedChangeOutputs.fee, | ||
inputs, | ||
outputs | ||
} | ||
}; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './GreedyInputSelector'; |
13 changes: 8 additions & 5 deletions
13
packages/input-selection/src/RoundRobinRandomImprove/change.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
129 changes: 0 additions & 129 deletions
129
packages/input-selection/src/RoundRobinRandomImprove/util.ts
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
export * from './RoundRobinRandomImprove'; | ||
export * from './GreedySelection'; | ||
export * from './types'; | ||
export * from './InputSelectionError'; |
Oops, something went wrong.