Introducing new QuickTx API in Cardano Client Lib 0.5.0-beta1

Satya
7 min readAug 18, 2023

--

With the release of Cardano Client Lib 0.5.0-beta1, we now have a new transaction builder API known as QuickTx. This post aims to provide an overview of this new API and demonstrate its utility.

Historically, Cardano Client Lib began with a high-level API for building payment or token mint transactions. While this provided simplicity, it lacked flexibility and support for more advanced transactions, such as those involving smart contracts.

To bridge this gap, the Composable Functions API was later introduced, offering flexibility and supporting various transaction types. This API provided out-of-the-box composable functions and allowed developers to write their custom functions for the TxBuilder API. However, it compromised on simplicity.

Enter QuickTx. This new transaction builder API strikes a balance between simplicity and flexibility. It’s built upon the Composable Functions API and offers a more streamlined experience, supporting an extensive range of transactions. Those familiar with the Lucid JS library will notice many similarities.

High-Level Concepts — QuickTx

Before diving into examples, let’s first explore some foundational concepts of QuickTx.

QuickTx API revolves around two main classes — Tx and ScriptTx – to declare different parts of a transaction or transaction fragments. These fragments are later composed by a new builder class, QuickTxBuilder, to construct the final transaction.

  1. Tx

This class is for transactions from regular (non-script) addresses. You can declaratively define a transaction from an address using this class. Each instance must have a unique sender. Transaction types for the Tx class include:

  • Transfers (address-to-address, non-script to script for fund locking)
  • Token operations (minting, burning)
  • Stake address operations (registration, de-registration, delegation)
  • Pool operations (registration, update, retirement)
  • Reward withdrawal

2. ScriptTx

This class can be used for transactions where a script witness is needed, like for transactions from a script address. With one ScriptTx instance, multiple script calls (spending, minting, etc.) can be declared together.

3. QuickTxBuilder

This class allows composition of multiple Tx and ScriptTx instances to create the final transaction. Beyond building the transaction, QuickTxBuilder also supports transaction signing and submission. Through Txcontxt, callbacks like “postBalanceTx” and “preBalanceTx” allow insertion of custom TxBuilder functions for application-specific customization.

Enough theory, let’s delve into some examples.

Setup and Pre-requisites

Accounts

For transactions, we need accounts. Here, we’ll set up a few testnet accounts for our examples:

Account sender1 = new Account(Networks.testnet(), mnemonic);
String sender1Addr = sender1.baseAddress();

Account sender2 = new Account(Networks.testnet(), mnemonic);
String sender2Addr = sender2.baseAddress();

String receiver1 = new Account(Networks.testnet(), mnemonic).baseAddress();

You can use any of the public testnets, such as Preprod or Preview. Please send some test Ada from the corresponding testnet faucet.

Backend Service

Create a BackendService instance for any of the supported backends (Blockfrost, Koios, Ogmios/Kupo, etc.). Alternatively, you can create your own backend implementation by implementing supplier interfaces such as UtxoSupplier, ProtocolParamsSupplier, and TransactionProcessor. However, to keep this blog post simple, let’s use one of the out-of-the-box providers

var backendService = new BFBackendService(Constants.BLOCKFROST_PREPROD_URL, bfProjectId);

or

var backendService = new KoiosBackendService(KOIOS_PREPROD_URL);

or

var backendService = new KupmiosBackendService(ogmiosUrl, kupoUrl)

Transactions with QuickTx

Having set up the account and backend service correctly, we are now prepared to submit our first transaction.

  1. A Simple Transfer from Sender to Receiver

Send test Ada from sender1 to receiver1 and attach a CIP20 message as metadata.

  • Declare a transaction to pay Ada to receiver1.
Tx tx = new Tx()
.payToAddress(receiver1, Amount.ada(5))
.attachMetadata(MessageMetadata.create().add("This is a test message"))
.from(sender1Addr);

Note: The from field must be set for Tx

  • Build and submit the transaction to Cardano network
QuickTxBuilder quickTxBuilder = new QuickTxBuilder(backendService);
Result<String> result = quickTxBuilder.compose(tx)
.withSigner(SignerProviders.signerFrom(sender1))
.complete();

The QuickTxBuilder class constructs the final transaction using tx, signs it, and submits it to a Cardano node through backend service. If the transaction is valid, result.getValue() returns the transaction hash. To wait for the transaction to be included in a block, utilize the completeAndWait function:

Result<String> result = quickTxBuilder.compose(tx)
.withSigner(SignerProviders.signerFrom(sender1))
.completeAndWait(System.out::println);

2. Send Transfer to Two Separate Addresses :- Composing Two Payments

Transfer test Ada from sender1 to receiver1 and from sender2 to receiver2 in one transaction.

  • Declarations:
Tx tx1 = new Tx()
.payToAddress(receiver1, Amount.ada(5))
.from(sender1Addr);

Tx tx2 = new Tx()
.payToAddress(receiver2, Amount.ada(4.5))
.from(sender2Addr);
  • Compose and submit:
Result<String> result = quickTxBuilder.compose(tx1, tx2)
.feePayer(sender1Addr)
.withSigner(SignerProviders.signerFrom(sender1))
.withSigner(SignerProviders.signerFrom(sender2))
.completeAndWait(System.out::println);

Note: The feePayer() method sets sender1Addr as the fee payer, a requisite step when merging multiple Tx transactions.

3. Token Minting

  • Setup — Create a policy. Define asset name and quantity
Policy policy = PolicyUtil.createMultiSigScriptAtLeastPolicy("test_policy", 1, 1);
String assetName = "MyAsset";
BigInteger qty = BigInteger.valueOf(200);
  • Declare minting transaction

In the code snippet below:

a. Create a new Tx.
b. Declare mintAsset with required information such as policyScript, asset, and the receiver address which will receive the newly minted token.
c. Define a ‘from’ address.

Tx tx = new Tx()
.mintAssets(policy.getPolicyScript(), new Asset(assetName, qty), sender1Addr)
.attachMetadata(MessageMetadata.create().add("Minting tx"))
.from(sender1Addr);
  • Build, Sign and Submit transaction

To build and submit a transaction, the steps are exactly the same. Use QuickTxBuilder to compose the Tx, and sign the transaction with both the sender1 account and the policy script.

Result<String> result = quickTxBuilder.compose(tx)
.withSigner(SignerProviders.signerFrom(sender1))
.withSigner(SignerProviders.signerFrom(policy))
.completeAndWait(System.out::println);

4. Minting Tokens and Distributing to Two Addresses

  • Declare transaction
BigInteger qty = BigInteger.valueOf(200);
Tx tx = new Tx()
.mintAssets(policy.getPolicyScript(), new Asset(assetName, qty))
.payToAddress(receiver1, Amount.asset(policy.getPolicyId(), assetName, 100))
.payToAddress(receiver2, Amount.asset(policy.getPolicyId(), assetName, 100))
.from(sender1Addr);

Mint 200 tokens and transfer 100 tokens each to receiver1 and receiver2.

5. Mint and Pay using Two Separate Senders

  • Declare transactions
Tx tx1 = new Tx()
.payToAddress(receiver1, Amount.ada(1.5))
.mintAssets(policy.getPolicyScript(), new Asset(assetName, qty), receiver2)
.from(sender1Addr);

Tx tx2 = new Tx()
.payToAddress(receiver2, Amount.ada(3))
.from(sender2Addr);

The first transaction (tx1) deducts 1.5 ADA from the sender1 address to pay receiver1, and it mints 200 new tokens which are also sent to receiver2.

The second transaction (tx2) pays receiver2 3 ADA.

  • Compose to build transaction and submit
Result<String> result = quickTxBuilder.compose(tx1, tx2)
.feePayer(sender1.baseAddress())
.withSigner(SignerProviders.signerFrom(sender1))
.withSigner(SignerProviders.signerFrom(sender2))
.withSigner(SignerProviders.signerFrom(policy))
.completeAndWait(System.out::println);

Both Tx instances are combined to construct the final transaction. Note that we are signing the transaction with three signers: Sender1, Sender2, and the policy script.

6. Stake Address Registration

For tasks like stake key registration, de-registration, and delegation, QuickTx significantly simplifies the process.

  • Registering the stake address:
Tx tx = new Tx()
.registerStakeAddress(sender1Addr)
.from(sender1Addr);

Result<String> result = quickTxBuilder.compose(tx)
.withSigner(SignerProviders.signerFrom(sender1))
.completeAndWait(msg -> System.out.println(msg));

7. Stake Key De-Registration

The transaction below will deregister the stake key and send the deposit back to sender1Addr. To de-register stake key, sign with account’s stake key and for fee payment, sign with account.

 Tx tx = new Tx()
.deregisterStakeAddress(sender1Addr)
.from(sender1Addr);

Result<String> result = quickTxBuilder.compose(tx)
.withSigner(SignerProviders.signerFrom(sender1))
.withSigner(SignerProviders.stakeKeySignerFrom(sender1))
.completeAndWait(msg -> System.out.println(msg));

For delegation and reward withdrawal, you can use delegateTo and withdraw methods, respectively.

Script / Smart Contract Transactions

In this section, we will explore some basic examples of smart contract transactions using QuickTx. This is just an introduction, and I’ll provide a more detailed step-by-step guide later in a separate blog post.

Pr-requisites

For smart contract transactions, you need both a PlutusScript and a script address. You can obtain a contract address from a PlutusScript object using the AddressProvider.

PlutusV2Script plutusScript = PlutusV2Script.builder()
.type("PlutusScriptV2")
.cborHex("49480100002221200101")
.build();

The script above represents the well-known alwaysTrue script that always returns true. To retrieve the script address:

String scriptAddress = AddressProvider.getEntAddress(plutusScript, Networks.testnet()).toBech32();

Note: For mainnet addresses, use Networks.mainnet().

1. Lock Fund

To secure funds at a script address with inline datum, use the payToContract() method from the Tx class:

BigIntPlutusData datumData = BigIntPlutusData.of(40);

Tx tx = new Tx()
.payToContract(scriptAddress, Amount.ada(10), datumData)
.from(sender2Addr);

Result<String> txResult = quickTxBuilder.compose(tx)
.withSigner(SignerProviders.signerFrom(sender2))
.completeAndWait(System.out::println);

In the code above, we are attempting to lock 10 Ada and datum 40 at the script address. The secured Ada can now only be accessed via a smart contract or script transaction.

2. Unlock funds at a contract address

Initially, find the UTXO(s) where the value is locked. Use the ScriptUtxoFinders class to achieve this:

UtxoSupplier utxoSupplier = new DefaultUtxoSupplier(backendService.getUtxoService());
Optional<Utxo> optionalUtxo = ScriptUtxoFinders.findFirstByInlineDatum(utxoSupplier, scriptAddress, datumData);

Now create a ScriptTx instance:

PlutusData redeemer = PlutusData.unit(); //any data for this contract
ScriptTx scriptTx = new ScriptTx()
.collectFrom(optionalUtxo.get(), redeemer)
.payToAddress(receiver1, Amount.ada(10))
.attachSpendingValidator(plutusScript);

In the above snippet, the collectFrom() method declares the input utxo(s) for this script transaction. The second parameter of the collectFrom method accepts redeemer data.

Next, we transfer the unlocked funds to receiver1. Finally, a spending validator can be attached using the attachSpendingValidator method.

To partially transfer locked funds to a receiver, you can use the withChangeAddress(address, datum) method. This refunds the remaining amount either to the script address or to any other specified address.

You can now build and submit the transaction using the familiar QuickTxBuilder:

Result<String> result = quickTxBuilder.compose(scriptTx)
.feePayer(sender1Addr)
.withSigner(SignerProviders.signerFrom(sender1))
.completeAndWait(System.out::println);

Remember, QuickTxBuilder automatically evaluates script transactions to calculate script cost by invoking the evaluateTx method from backend service.

However, for in-app script cost evaluations without backend service dependency, you can use AikenTransactionEvaluator.

ProtocolParamsSupplier protocolParamsSupplier = new DefaultProtocolParamsSupplier(backendService.getEpochService());
Result<String> result = quickTxBuilder.compose(scriptTx)
.feePayer(sender1Addr)
.withSigner(SignerProviders.signerFrom(sender1))
.withTxEvaluator(new AikenTransactionEvaluator(utxoSupplier, protocolParamsSupplier))
.completeAndWait(System.out::println);

Note: It’s possible to incorporate multiple script calls or attach multiple spending validators in one ScriptTx instance.

2. Mint tokens with Script

To mint tokens using PlutusScript, initiate a ScriptTx instance and invoke mintAsset(). The following example demonstrates minting three separate tokens:

ScriptTx scriptTx = new ScriptTx()
.mintAsset(plutusScript1, asset1, BigIntPlutusData.of(1), receiver1)
.mintAsset(plutusScript2, asset2, BigIntPlutusData.of(2), sender1Addr)
.mintAsset(plutusScript3, asset3, BigIntPlutusData.of(3), receiver1)
.withChangeAddress(sender1Addr);

Result<String> result1 = quickTxBuilder.compose(scriptTx)
.feePayer(sender1Addr)
.withSigner(SignerProviders.signerFrom(sender1))
.completeAndWait(System.out::println);

With ScriptTx, you can also handle reference inputs and execute various transaction types.

Conclusion

Thank you for taking the time to go through this guide with me. Stay tuned for more in-depth tutorials on working with Cardano Client Lib. If you have any questions or feedback, please don’t hesitate to reach out.

Happy coding!

Resources

  1. Cardano Client Lib GitHub — https://github.com/bloxbean/cardano-client-lib
  2. Cardano Client Lib Docs — https://cardano-client.dev

--

--