Skip to content

Commit

Permalink
feat: #490 Add AddressIterator support in balance, change outputs adj…
Browse files Browse the repository at this point in the history
…ustment and collateral builder
  • Loading branch information
satran004 committed Feb 17, 2025
1 parent e49655a commit 02f28da
Show file tree
Hide file tree
Showing 3 changed files with 101 additions and 13 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.bloxbean.cardano.client.function.helper;

import com.bloxbean.cardano.client.api.AddressIterator;
import com.bloxbean.cardano.client.api.common.AddressIterators;
import com.bloxbean.cardano.client.function.TxBuilder;
import com.bloxbean.cardano.client.api.util.UtxoUtil;
import lombok.extern.slf4j.Slf4j;
Expand All @@ -20,11 +22,31 @@ public class BalanceTxBuilders {
* @return TxBuilder
*/
public static TxBuilder balanceTx(String changeAddress, int nSigners) {
return balanceTx(AddressIterators.of(changeAddress), nSigners);
}

/**
* Function to balance an unbalanced transaction.
* This is a wrapper function which invokes the following functions to create a balanced transaction
* <ul>
* <li>{@link FeeCalculators#feeCalculator(String, int)} </li>
* <li>{@link ChangeOutputAdjustments#adjustChangeOutput(String, int)} </li>
* <li>{@link CollateralBuilders#balanceCollateralOutputs()} (For transaction with collateral return)</li>
* </ul>
*
* @param changeAddressIter An iterator for change addresses to be used for adjusting outputs.
* The first address from the iterator is used as the change address
* @param nSigners No of signers. This is required for accurate fee calculation.
* @return A {@link TxBuilder} instance that balances the transaction during its building phase.
*/
public static TxBuilder balanceTx(AddressIterator changeAddressIter, int nSigners) {
return (context, txn) -> {
String changeAddress = changeAddressIter.getFirst().getAddress();

FeeCalculators.feeCalculator(changeAddress, nSigners).apply(context, txn);

//Incase change output goes below min ada after fee deduction
ChangeOutputAdjustments.adjustChangeOutput(changeAddress, nSigners).apply(context, txn);
ChangeOutputAdjustments.adjustChangeOutput(changeAddressIter.clone(), changeAddress, nSigners).apply(context, txn);

//If collateral return found, balance collateral outputs
if (txn.getBody().getCollateralReturn() != null)
Expand Down Expand Up @@ -55,16 +77,40 @@ public static TxBuilder balanceTx(String changeAddress) {
* <li>{@link ChangeOutputAdjustments#adjustChangeOutput(String, int)} </li>
* <li>{@link CollateralBuilders#balanceCollateralOutputs()} (For transaction with collateral return)</li>
* </ul>
*
* @param changeAddress Change output address
* @param additionalSigners No of Additional signers. This is required for accurate fee calculation.
* @return TxBuilder
*/
public static TxBuilder balanceTxWithAdditionalSigners(String changeAddress, int additionalSigners) {
return balanceTxWithAdditionalSigners(AddressIterators.of(changeAddress), additionalSigners);
}

/**
* Function to balance an unbalanced transaction using Automatic Utxo Discovery with Additional Signers.
* This is a wrapper function which invokes the following functions to create a balanced transaction
* <ul>
* <li>{@link FeeCalculators#feeCalculator(String, int)} </li>
* <li>{@link ChangeOutputAdjustments#adjustChangeOutput(String, int)} </li>
* <li>{@link CollateralBuilders#balanceCollateralOutputs()} (For transaction with collateral return)</li>
* </ul>
*
* @param changeAddressIter An iterator to provide addresses for any change output.
* The first address from the iterator is used as the change address
* @param additionalSigners The number of additional required signers to include in the
* transaction balancing process.
* @return A {@link TxBuilder} function that applies the necessary adjustments to balance
* the transaction, including fee calculation, change output adjustments, and
* collateral balancing if applicable.
*/
public static TxBuilder balanceTxWithAdditionalSigners(AddressIterator changeAddressIter, int additionalSigners) {
return (context, txn) -> {
FeeCalculators.feeCalculator(changeAddress, UtxoUtil.getNoOfRequiredSigners(context.getUtxos()) + additionalSigners).apply(context, txn);
String changeAddress = changeAddressIter.getFirst().getAddress();

FeeCalculators.feeCalculator(changeAddress, UtxoUtil.getNoOfRequiredSigners(context.getAllUtxos()) + additionalSigners).apply(context, txn);

//Incase change output goes below min ada after fee deduction
ChangeOutputAdjustments.adjustChangeOutput(changeAddress, UtxoUtil.getNoOfRequiredSigners(context.getUtxos()) + additionalSigners).apply(context, txn);
ChangeOutputAdjustments.adjustChangeOutput(changeAddressIter.clone(), UtxoUtil.getNoOfRequiredSigners(context.getAllUtxos()) + additionalSigners).apply(context, txn);

//If collateral return found, balance collateral outputs
if (txn.getBody().getCollateralReturn() != null)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
package com.bloxbean.cardano.client.function.helper;

import com.bloxbean.cardano.client.address.Address;
import com.bloxbean.cardano.client.api.AddressIterator;
import com.bloxbean.cardano.client.api.common.AddressIterators;
import com.bloxbean.cardano.client.api.exception.ApiException;
import com.bloxbean.cardano.client.api.exception.ApiRuntimeException;
import com.bloxbean.cardano.client.api.model.Utxo;
import com.bloxbean.cardano.client.api.model.WalletUtxo;
import com.bloxbean.cardano.client.coinselection.UtxoSelector;
import com.bloxbean.cardano.client.function.MinAdaChecker;
import com.bloxbean.cardano.client.function.TxBuilder;
Expand Down Expand Up @@ -52,7 +56,7 @@ public class ChangeOutputAdjustments {
* @throws ApiRuntimeException If api call error
*/
public static TxBuilder adjustChangeOutput(String changeAddress) {
return adjustChangeOutput(changeAddress, changeAddress, 1);
return adjustChangeOutput(AddressIterators.of(changeAddress), changeAddress, 1);
}

/**
Expand All @@ -73,7 +77,29 @@ public static TxBuilder adjustChangeOutput(String changeAddress) {
* @throws ApiRuntimeException If api call error
*/
public static TxBuilder adjustChangeOutput(String changeAddress, int noOfSigners) {
return adjustChangeOutput(changeAddress, changeAddress, noOfSigners);
return adjustChangeOutput(AddressIterators.of(changeAddress), changeAddress, noOfSigners);
}

/**
* Function to adjust change output in a <code>Transaction</code> to meet min ada requirement.
* Finds a change output specific to given change address.
* If multiple change outputs with less than min required ada are found for the change address, it throws <code>{@link TxBuildException}</code>
* Get additional utxos from change address and update the change output.
* Re-calculates fee and checks min ada in change output.
* Retry if required, upto 3 times.
* <p>
* <br>Default values:
* <br> Change Output Address = First address in senderAddressIter
* </p>
*
* @param senderAddressIter Addresses to select additional utxos. The first address in the iterator is selected as change output address.
* @param noOfSigners The number of signers required for the transaction.
* @return A TxBuilder instance with the modified change output configuration.
*/
public static TxBuilder adjustChangeOutput(AddressIterator senderAddressIter, int noOfSigners) {
var changeAddress = senderAddressIter.getFirst().getAddress();

return adjustChangeOutput(senderAddressIter.clone(), changeAddress, noOfSigners);
}

/**
Expand All @@ -84,14 +110,14 @@ public static TxBuilder adjustChangeOutput(String changeAddress, int noOfSigners
* Re-calculates fee and checks min ada in change output.
* Retry if required, upto 3 times.
*
* @param senderAddress Address to select additional utxos
* @param senderAddressIter Addresses to select additional utxos
* @param changeAddress Address for change output selection
* @param noOfSigners No of required signers. Required for fee calculation after adjustment
* @return <code>TxBuilder</code> function
* @throws TxBuildException If multiple change outputs with less than min required ada are found for the change address.
* @throws ApiRuntimeException If api call error
*/
public static TxBuilder adjustChangeOutput(String senderAddress, String changeAddress, int noOfSigners) {
public static TxBuilder adjustChangeOutput(AddressIterator senderAddressIter, String changeAddress, int noOfSigners) {
return (context, transaction) -> {
int counter = 0;
while (true) {
Expand Down Expand Up @@ -124,7 +150,7 @@ else if (outputsWithLessAda.size() > 1) {

//check outputs for minAda and balance if required
try {
adjust(context, transaction, outputsWithLessAda.get(0)._1, outputsWithLessAda.get(0)._2, senderAddress, changeAddress);
adjust(context, transaction, outputsWithLessAda.get(0)._1, outputsWithLessAda.get(0)._2, senderAddressIter.clone());
} catch (ApiException apiException) {
throw new ApiRuntimeException("Error in api call", apiException);
}
Expand All @@ -137,7 +163,7 @@ else if (outputsWithLessAda.size() > 1) {
}

private static void adjust(TxBuilderContext context, Transaction transaction, TransactionOutput outputToAdjust, BigInteger additionalRequiredAmt,
String senderAddress, String changeAddress) throws ApiException {
AddressIterator senderAddressItr) throws ApiException {
Objects.requireNonNull(context);
Objects.requireNonNull(transaction);

Expand Down Expand Up @@ -171,22 +197,32 @@ private static void adjust(TxBuilderContext context, Transaction transaction, Tr

List<Utxo> newUtxos = new ArrayList<>();
UtxoSelector utxoSelector = context.getUtxoSelector();

Address sender = senderAddressItr.getFirst();
String senderAddr = sender.getAddress();

//Try to find ada only utxo
Optional<Utxo> utxoOptional = utxoSelector.findFirst(senderAddress, utxo ->
Optional<Utxo> utxoOptional = utxoSelector.findFirst(senderAddr, utxo ->
!existingUtxos.contains(utxo) && utxo.getAmount().size() == 1
&& utxo.getAmount().get(0).getQuantity().compareTo(totalRequiredWithBuffer) == 1);

if (utxoOptional.isPresent()) {
newUtxos.add(utxoOptional.get());
if (sender.getDerivationPath().isPresent()) {
var walletUtxo = WalletUtxo.from(utxoOptional.get());
walletUtxo.setDerivationPath(sender.getDerivationPath().get());
newUtxos.add(walletUtxo);
} else {
newUtxos.add(utxoOptional.get());
}
} else { //Not Found
//Use utxo selection strategy
List<Utxo> utxosFound = null;

try {
utxosFound = context.getUtxoSelectionStrategy().selectUtxos(senderAddress, LOVELACE, totalRequiredWithBuffer, existingUtxos);
utxosFound = context.getUtxoSelectionStrategy().selectUtxos(senderAddressItr.clone(), LOVELACE, totalRequiredWithBuffer, existingUtxos);
} catch (ApiException ex) {
//Not found... check without Buffer
utxosFound = context.getUtxoSelectionStrategy().selectUtxos(senderAddress, LOVELACE, additionalRequiredAmt, existingUtxos);
utxosFound = context.getUtxoSelectionStrategy().selectUtxos(senderAddressItr.clone(), LOVELACE, additionalRequiredAmt, existingUtxos);
}

if (utxosFound != null && utxosFound.size() > 0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ public static TxBuilder collateralFrom(List<Utxo> utxos) {
.index(utxo.getOutputIndex())
.build();
transaction.getBody().getCollateral().add(input);

context.addCollateralUtxo(utxo);
});
};
}
Expand All @@ -54,6 +56,8 @@ public static TxBuilder collateralFrom(Supplier<List<Utxo>> supplier) {
.index(utxo.getOutputIndex())
.build();
transaction.getBody().getCollateral().add(input);

context.addCollateralUtxo(utxo);
});
};
}
Expand Down Expand Up @@ -102,6 +106,8 @@ public static TxBuilder collateralOutputs(String collateralReturnAddress, List<U

//Create collateral output
UtxoUtil.copyUtxoValuesToOutput(collateralOutput, utxo);

context.addCollateralUtxo(utxo);
});

transaction.getBody().setCollateralReturn(collateralOutput);
Expand Down

0 comments on commit 02f28da

Please sign in to comment.