Skip to content

Commit

Permalink
fixup! feat(input-selection): added new greedy input selector
Browse files Browse the repository at this point in the history
  • Loading branch information
AngelCastilloB committed May 30, 2023
1 parent ba1e3a3 commit 6d180e1
Show file tree
Hide file tree
Showing 5 changed files with 246 additions and 62 deletions.
1 change: 1 addition & 0 deletions packages/input-selection/package.json
Expand Up @@ -68,6 +68,7 @@
"dependencies": {
"@cardano-sdk/core": "workspace:~",
"@cardano-sdk/util": "workspace:~",
"bignumber.js": "^9.1.1",
"lodash": "^4.17.21",
"ts-custom-error": "^3.2.0"
},
Expand Down
102 changes: 44 additions & 58 deletions packages/input-selection/src/GreedySelection/GreedyInputSelector.ts
@@ -1,17 +1,19 @@
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 {
getAssetAddition,
getAssetDifference,
getAssetQuantities,
getCoinQuantity,
hasNegativeAssetValue,
isValidValue,
sortByCoins,
splitChange,
toValues
} from '../util';

type AdjustedOutputs = { outputs: Set<Cardano.TxOut>; fee: bigint; change: Set<Cardano.Value> };
type AdjustedOutputs = { fee: bigint; change: Array<Cardano.TxOut> };

interface GreedySelectorProps {
getChangeAddresses: () => Promise<Map<Cardano.PaymentAddress, number>>;
Expand All @@ -25,38 +27,32 @@ interface GreedySelectorProps {
* @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.
* @param change Change value if any.
*/
const adjustOutputsForFee = async (
constraints: SelectionConstraints,
inputs: Set<Cardano.Utxo>,
outputs: Set<Cardano.TxOut>,
currentFee: bigint,
change?: Cardano.Value
changeOutputs: Array<Cardano.TxOut>,
currentFee: bigint
): Promise<AdjustedOutputs> => {
let result = { change: new Set<Cardano.Value>(), fee: currentFee, outputs };
// const changeBundles = new Set<Cardano.Value>();
// const changeBundlesByAddress = new Map<Cardano.PaymentAddress, Set<Cardano.Value>>();

// if (change) changeBundles.add(change);

// Compute the fee. If the current fee is less than the newly computed fee, deduct the difference from the 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.
// Calculate fee with change outputs that include fee.
// It will cover the fee of final selection,
// where fee is excluded from change bundles
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
outputs: totalOutputs
});

if (currentFee < fee) {
const feeDifference = fee - currentFee;
const updatedOutputs = [...outputs];
const updatedOutputs = [...changeOutputs];

updatedOutputs.sort(sortByCoins);

Expand All @@ -73,7 +69,7 @@ const adjustOutputsForFee = async (

if (!feeAccountedFor) throw new InputSelectionError(InputSelectionFailure.UtxoBalanceInsufficient);

result = await adjustOutputsForFee(constraints, inputs, new Set(updatedOutputs), fee, change);
result = await adjustOutputsForFee(constraints, inputs, outputs, updatedOutputs, fee);
}

return result;
Expand All @@ -93,63 +89,53 @@ const adjustOutputsForFee = async (
* the user has too many native assets.
*/
export class GreedyInputSelector implements InputSelector {
getChangeAddresses: GreedySelectorProps;
#props: GreedySelectorProps;

constructor(props: GreedySelectorProps) {
this.getChangeAddresses = props;
this.#props = props;
}

async select(params: InputSelectionParameters): Promise<SelectionResult> {
const { utxo: inputs, outputs, constraints } = params;
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(utxoValues);
// const changeBundlesByAddress = new Map<Cardano.PaymentAddress, Set<Cardano.Value>>();

if (totalLovelaceInUtxoSet === totalLovelaceInOutputSet && totalAssetsInUtxoSet === totalAssetsInOutputSet) {
// The wallet is trying to spend the whole balance, we just need to account for the fee and return the modified selection.
const adjustedOutputs = await adjustOutputsForFee(constraints, inputs, outputs, 0n);

return {
remainingUTxO: new Set<Cardano.Utxo>(),
selection: {
change: [],
fee: adjustedOutputs.fee,
inputs,
outputs: adjustedOutputs.outputs
}
};
}

// The wallet did not spend all the balance, so we must add the remaining as change.
const changeLovelace = totalLovelaceInOutputSet - totalLovelaceInUtxoSet;
const changeAssets = getAssetDifference(totalAssetsInOutputSet, totalAssetsInUtxoSet);

// If the wallet tries to spend more than we can cover with the current UTXO set throw.
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 = getAssetAddition(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 = totalLovelaceOutput - totalLovelaceInput;
const changeAssets = getAssetDifference(totalAssetsInOutputSet, totalAssetsInput);

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

const change = {
assets: changeAssets.size === 0 ? undefined : changeAssets,
coins: changeLovelace
};

// If not enough balance remaining to create a valid change output throw.
if (!isValidValue(change, constraints.computeMinimumCoinQuantity, constraints.tokenBundleSizeExceedsLimit))
throw new InputSelectionError(InputSelectionFailure.UtxoBalanceInsufficient);

const adjustedOutputs = await adjustOutputsForFee(constraints, inputs, outputs, 0n, change);
// 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
);

const adjustedChangeOutputs = await adjustOutputsForFee(constraints, inputs, outputs, changeOutputs, 0n);
return {
remainingUTxO: new Set<Cardano.Utxo>(),
selection: {
change: [],
fee: adjustedOutputs.fee,
change: adjustedChangeOutputs.change,
fee: adjustedChangeOutputs.fee,
inputs,
outputs: adjustedOutputs.outputs
outputs
}
};
}
Expand Down

0 comments on commit 6d180e1

Please sign in to comment.