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
f5f997e
commit 7a9cc77
Showing
14 changed files
with
1,546 additions
and
175 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
139 changes: 139 additions & 0 deletions
139
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,139 @@ | ||
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, Cardano.Percent>>; | ||
} | ||
|
||
/** | ||
* Given a set of input and outputs, compute the fee. Then extract the fee from the change output | ||
* with the highest value. | ||
* | ||
* @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 | ||
* as change. | ||
*/ | ||
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
167 changes: 0 additions & 167 deletions
167
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.