Skip to content

Commit

Permalink
#446 Support for CompletableFuture in QuickTxBuilder (#476)
Browse files Browse the repository at this point in the history
* Refactor transaction result handling with TxResult and TxStatus

Replaced Constant class with a new TxResult class and TxStatus enum for better transaction result representation and status tracking. Updated QuickTxBuilder methods to return TxResult instead of Result<String>, incorporating status metadata. Adjusted related test cases and methods to align with the new structure.
  • Loading branch information
satran004 authored Dec 17, 2024
1 parent 9ac1c08 commit e13b11e
Show file tree
Hide file tree
Showing 6 changed files with 225 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ public class Result<T> {
int code;
T value;

private Result(boolean successful) {
protected Result(boolean successful) {
this.successful = successful;
}

private Result(boolean successful, String response) {
protected Result(boolean successful, String response) {
this.successful = successful;
this.response = response;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
import java.util.List;
import java.util.Optional;
import java.util.Random;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertTrue;
Expand Down Expand Up @@ -46,9 +48,11 @@ void alwaysTrueScript() throws ApiException {
.from(sender2Addr);

QuickTxBuilder quickTxBuilder = new QuickTxBuilder(backendService);
Result<String> result = quickTxBuilder.compose(tx)
var result = quickTxBuilder.compose(tx)
.withSigner(SignerProviders.signerFrom(sender2))
.completeAndWait(System.out::println);
.complete();

assertThat(result.getTxStatus()).isEqualTo(TxStatus.SUBMITTED);

System.out.println(result.getResponse());
checkIfUtxoAvailable(result.getValue(), scriptAddress);
Expand Down Expand Up @@ -415,4 +419,58 @@ void alwaysTrueScript_whenInputWithRefScriptAndSameScriptAsAttachedValidator_rem
checkIfUtxoAvailable(result1.getValue(), sender2Addr);
}

@Test
void alwaysTrueScript_withCompleteAsync() throws ApiException, ExecutionException, InterruptedException {
PlutusV3Script plutusScript = PlutusV3Script.builder()
.type("PlutusScriptV3")
.cborHex("46450101002499")
.build();

String scriptAddress = AddressProvider.getEntAddress(plutusScript, Networks.testnet()).toBech32();
BigInteger scriptAmt = new BigInteger("2479280");

long randInt = System.currentTimeMillis();
BigIntPlutusData plutusData = new BigIntPlutusData(BigInteger.valueOf(randInt)); //any random number

Tx tx = new Tx();
tx.payToContract(scriptAddress, Amount.lovelace(scriptAmt), plutusData)
.from(sender2Addr);

QuickTxBuilder quickTxBuilder = new QuickTxBuilder(backendService);
var future = quickTxBuilder.compose(tx)
.withSigner(SignerProviders.signerFrom(sender2))
.completeAndWaitAsync(System.out::println);

var result = future.get();

System.out.println(result.getResponse());
checkIfUtxoAvailable(result.getValue(), scriptAddress);

Optional<Utxo> optionalUtxo = ScriptUtxoFinders.findFirstByInlineDatum(utxoSupplier, scriptAddress, plutusData);
ScriptTx scriptTx = new ScriptTx()
.collectFrom(optionalUtxo.get(), plutusData)
.payToAddress(receiver1, Amount.lovelace(scriptAmt))
.attachSpendingValidator(plutusScript)
.withChangeAddress(scriptAddress, plutusData);

future = quickTxBuilder.compose(scriptTx)
.feePayer(sender2Addr)
.withSigner(SignerProviders.signerFrom(sender2))
.withRequiredSigners(sender2.getBaseAddress())
.withVerifier(txn -> {
System.out.println(JsonUtil.getPrettyJson(txn));
assertThat(txn.getBody().getRequiredSigners()).hasSize(1);
assertThat(txn.getBody().getRequiredSigners().get(0)) //Verify sender's payment cred hash in required signer
.isEqualTo(sender2.getBaseAddress().getPaymentCredentialHash().get());
})
.completeAndWaitAsync(System.out::println, Executors.newSingleThreadExecutor());

var result1 = future.get();
System.out.println(result1);
assertTrue(result1.isSuccessful());
System.out.println(result1.getTxStatus());

checkIfUtxoAvailable(result1.getValue(), sender2Addr);
}

}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
import java.time.Duration;
import java.time.Instant;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.function.Consumer;
import java.util.stream.Collectors;

Expand Down Expand Up @@ -454,7 +456,7 @@ private TxBuilder addRequiredSignersBuilder() {
*
* @return Result of transaction submission
*/
public Result<String> complete() {
public TxResult complete() {
if (txList.length == 0)
throw new TxBuildException("At least one tx is required");

Expand All @@ -475,7 +477,7 @@ public Result<String> complete() {
if (!result.isSuccessful()) {
log.error("Transaction : " + transaction);
}
return result;
return TxResult.fromResult(result).withTxStatus(TxStatus.SUBMITTED);
} catch (Exception e) {
throw new ApiRuntimeException(e);
}
Expand All @@ -487,7 +489,7 @@ public Result<String> complete() {
*
* @return Result of transaction submission
*/
public Result<String> completeAndWait() {
public TxResult completeAndWait() {
return completeAndWait(Duration.ofSeconds(60), (msg) -> log.info(msg));
}

Expand All @@ -498,7 +500,7 @@ public Result<String> completeAndWait() {
* @param logConsumer consumer to get log messages
* @return Result of transaction submission
*/
public Result<String> completeAndWait(Consumer<String> logConsumer) {
public TxResult completeAndWait(Consumer<String> logConsumer) {
return completeAndWait(Duration.ofSeconds(60), logConsumer);
}

Expand All @@ -508,7 +510,7 @@ public Result<String> completeAndWait(Consumer<String> logConsumer) {
* @param timeout Timeout to wait for transaction to be included in the block
* @return Result of transaction submission
*/
public Result<String> completeAndWait(Duration timeout) {
public TxResult completeAndWait(Duration timeout) {
return completeAndWait(timeout, Duration.ofSeconds(2), (msg) -> log.info(msg));
}

Expand All @@ -518,7 +520,7 @@ public Result<String> completeAndWait(Duration timeout) {
* @param logConsumer consumer to get log messages
* @return Result of transaction submission
*/
public Result<String> completeAndWait(Duration timeout, Consumer<String> logConsumer) {
public TxResult completeAndWait(Duration timeout, Consumer<String> logConsumer) {
return completeAndWait(timeout, Duration.ofSeconds(2), logConsumer);
}

Expand All @@ -529,32 +531,33 @@ public Result<String> completeAndWait(Duration timeout, Consumer<String> logCons
* @param logConsumer consumer to get log messages
* @return Result of transaction submission
*/
public Result<String> completeAndWait(@NonNull Duration timeout, @NonNull Duration checkInterval,
public TxResult completeAndWait(@NonNull Duration timeout, @NonNull Duration checkInterval,
@NonNull Consumer<String> logConsumer) {
Result<String> result = complete();
var txResult = TxResult.fromResult(result);
if (!result.isSuccessful())
return result;
return txResult.withTxStatus(TxStatus.FAILED);

Instant startInstant = Instant.now();
long millisToTimeout = timeout.toMillis();

logConsumer.accept(showStatus(Constant.STATUS_SUBMITTED, result.getValue()));
logConsumer.accept(showStatus(TxStatus.SUBMITTED, result.getValue()));
String txHash = result.getValue();
try {
if (result.isSuccessful()) { //Wait for transaction to be included in the block
int count = 0;
while (count < 60) {
Optional<Utxo> utxoOptional = utxoSupplier.getTxOutput(txHash, 0);
if (utxoOptional.isPresent()) {
logConsumer.accept(showStatus(Constant.STATUS_CONFIRMED, txHash));
return result;
logConsumer.accept(showStatus(TxStatus.CONFIRMED, txHash));
return txResult.withTxStatus(TxStatus.CONFIRMED);
}

logConsumer.accept(showStatus(Constant.STATUS_PENDING, txHash));
logConsumer.accept(showStatus(TxStatus.PENDING, txHash));
Instant now = Instant.now();
if (now.isAfter(startInstant.plusMillis(millisToTimeout))) {
logConsumer.accept(showStatus(Constant.STATUS_TIMEOUT, txHash));
return result;
logConsumer.accept(showStatus(TxStatus.TIMEOUT, txHash));
return txResult.withTxStatus(TxStatus.TIMEOUT);
}

Thread.sleep(checkInterval.toMillis());
Expand All @@ -565,11 +568,102 @@ public Result<String> completeAndWait(@NonNull Duration timeout, @NonNull Durati
logConsumer.accept("Error while waiting for transaction to be included in the block. TxHash : " + txHash);
}

logConsumer.accept(showStatus(Constant.STATUS_TIMEOUT, txHash));
return result;
logConsumer.accept(showStatus(TxStatus.PENDING, txHash));
return txResult.withTxStatus(TxStatus.PENDING);
}

private String showStatus(String status, String txHash) {
/**
* Completes the task and waits asynchronously with a specified timeout duration and a logging function.
*
* @return a CompletableFuture containing the result of the completion task.
*/
public CompletableFuture<TxResult> completeAndWaitAsync() {
return completeAndWaitAsync(Duration.ofSeconds(2), (msg) -> log.info(msg));
}

/**
* Submit the transaction and return a CompletableFuture containing a Result that wraps a txHash if the operation is successful.
*
* @param logConsumer a consumer that processes log messages. It must not be null.
* @return a CompletableFuture containing a Result that wraps txHash if the operation is successful.
*/
public CompletableFuture<TxResult> completeAndWaitAsync(@NonNull Consumer<String> logConsumer) {
return completeAndWaitAsync(Duration.ofSeconds(2), logConsumer);
}

/**
* Submit the transaction and return a CompletableFuture containing a Result that wraps a txHash if the operation is successful.
*
* @param logConsumer a consumer that processes log messages. It must not be null.
* @param executor the executor to use for asynchronous execution. It must not be null.
* @return a CompletableFuture containing a Result that wraps txHash if the operation is successful.
*/
public CompletableFuture<TxResult> completeAndWaitAsync(@NonNull Consumer<String> logConsumer, @NonNull Executor executor) {
return completeAndWaitAsync(Duration.ofSeconds(2), logConsumer, executor);
}

/**
* Submit the transaction and return a CompletableFuture containing a Result that wraps a txHash if the operation is successful.
*
* @param checkInterval the interval to check if the transaction is included in the block. It must not be null.
* @param logConsumer a consumer that processes log messages. It must not be null.
* @return a CompletableFuture containing a Result that wraps txHash if the operation is successful.
*/
public CompletableFuture<TxResult> completeAndWaitAsync(@NonNull Duration checkInterval,
@NonNull Consumer<String> logConsumer) {
return completeAndWaitAsync(checkInterval, logConsumer, null);
}

/**
* Submit the transaction and return a CompletableFuture containing a Result that wraps a txHash if the operation is successful.
*
* @param checkInterval the interval to check if the transaction is included in the block. It must not be null.
* @param logConsumer a consumer that processes log messages. It must not be null.
* @param executor the executor to use for asynchronous execution. It must not be null.
* @return a CompletableFuture containing a Result that wraps txHash if the operation is successful.
*/
public CompletableFuture<TxResult> completeAndWaitAsync(@NonNull Duration checkInterval,
@NonNull Consumer<String> logConsumer, Executor executor) {
if (executor != null) {
return CompletableFuture.supplyAsync(() -> waitForTxResult(checkInterval, logConsumer), executor);
} else {
return CompletableFuture.supplyAsync(() -> waitForTxResult(checkInterval, logConsumer));
}
}

private TxResult waitForTxResult(Duration checkInterval, Consumer<String> logConsumer) {
Result<String> result = complete();
var txResult = TxResult.fromResult(result);
if (!result.isSuccessful()) {
return txResult.withTxStatus(TxStatus.FAILED);
}

logConsumer.accept(showStatus(TxStatus.SUBMITTED, result.getValue()));
String txHash = result.getValue();
try {
if (result.isSuccessful()) { //Wait for transaction to be included in the block
while (true) {
Optional<Utxo> utxoOptional = utxoSupplier.getTxOutput(txHash, 0);
if (utxoOptional.isPresent()) {
logConsumer.accept(showStatus(TxStatus.CONFIRMED, txHash));
return txResult.withTxStatus(TxStatus.CONFIRMED);
}

Thread.sleep(checkInterval.toMillis());
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("Error while waiting for transaction to be included in the block. TxHash : " + txHash, e);
logConsumer.accept("Error while waiting for transaction to be included in the block. TxHash : " + txHash);
}

logConsumer.accept(showStatus(TxStatus.PENDING, txHash));
return txResult.withTxStatus(TxStatus.PENDING);
}


private String showStatus(TxStatus status, String txHash) {
return String.format("[%s] Tx: %s", status, txHash);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.bloxbean.cardano.client.quicktx;

import com.bloxbean.cardano.client.api.model.Result;

/**
* Represents the result of a transaction.
*
* This class extends the Result class specialized with the String type, providing
* additional transaction-specific functionality, such as associating a transaction
* status and retrieving transaction-specific details like transaction hash.
*/
public class TxResult extends Result<String> {
private TxStatus txStatus;

protected TxResult(boolean successful) {
super(successful);
}

protected TxResult(boolean successful, String response) {
super(successful, response);
}

public TxResult withTxStatus(TxStatus status) {
this.txStatus = status;
return this;
}

public TxStatus getTxStatus() {
return txStatus;
}

public String getTxHash() {
return getValue();
}

public static TxResult fromResult(Result<String> result) {
var txResult = new TxResult(result.isSuccessful(), result.getResponse());
txResult.withValue(result.getValue());

return txResult;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.bloxbean.cardano.client.quicktx;

public enum TxStatus {
SUBMITTED,
FAILED,
PENDING,
TIMEOUT,
CONFIRMED
}

0 comments on commit e13b11e

Please sign in to comment.