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.
- 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.
- 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 forTx
- 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 setssender1Addr
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 thesender1
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 payreceiver1
, and it mints 200 new tokens which are also sent toreceiver2
.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
andwithdraw
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
- Cardano Client Lib GitHub — https://github.com/bloxbean/cardano-client-lib
- Cardano Client Lib Docs — https://cardano-client.dev