Cardano

Call Plutus V2 contract from off-chain Java code using Cardano Client Lib

In this post, we will go through the steps required to invoke a Plutus smart contract from a Java application using Cardano Client Lib.

Cardano-client-lib is a Java client library for Cardano blockchain. It simplifies the interaction with Cardano blockchain from Java applications. Using this library, you can perform various types of transactions including smart contract calls in a Java application.

Compatible version: Cardano Client Lib 0.3.0-beta2 and above

Plutus Contract Example

We will use a simple contract called “Sum Contract” as an example in this post. To validate, the sum contract checks the sum of 0..n, where n is the datum value. The caller can unlock the fund in a script output by guessing the correct sum.

While this is a very simple plutus contract, but through this example we will try to use some of the new exciting features available in Babbage era (after Vasil Hardfork).

  1. Reference Inputs (CIP 31)
  2. Inline Datums (CIP 32)
  3. Reference Scripts (CIP 33)
  4. Collateral Output (CIP 40)

So, in high level we will be going through the following steps :

  1. Create a reference script output to attach a script to the output. This is part of CIP 33. With reference script output, we don’t need to send the script body in the spending transaction.
  2. Send some fund to script address with inline datum. The fund will be locked in the script output. Inline datum is defined in CIP 32. So instead of storing datum hash, we can now store datum value directly in the output.
  3. Finally, create a spending transaction to unlock fund in script output. In spending transaction, we will refer to the reference script output (step-1) as a reference input. This is part of CIP 31. Also, we are going to use collateral outputs (CIP 40) in the spending transaction.

Before going through our example, let’s discuss some basic concepts in Cardano Client Lib. For more details, you can check my previous posts.

Alternatively, you can jump to the section “Invoke Guess Sum Contract” if you are already aware of these concepts.

Composable Functions

A set of FunctionalInterface which can be used to implement composable functions. These functions are used to build various different types of transactions. The library provides many useful out-of-box implementations of these functions. Most of the commonly used functions are already there. If required, application developers can write their own functions and use them with other out of box functions.

The followings are the main FunctionalInterface

  1. TxBuilder
  2. TxOutputBuilder
  3. TxInputBuilder
  4. TxSigner

TxBuilder : This functional interface helps to transform a transaction object. The apply method in this interface takes a TxBuilderContext and a Transaction object as input parameters. The role of this function is to transform the input transaction object with additional attributes or update existing attributes.

TxOutputBuilder : This functional interface helps to build a TransactionOutput object and adds that to the transaction output list. The accept method in this interface takes a TxBuilderContext and a list of TransactionOutput.

TxInputBuilder : This functional interface is responsible to build inputs for the expected outputs.

TxSigner : This interface is responsible to provide transaction signing capability.

Backend Service and Supplier Interfaces

The library provides a backend service layer, which is required to build and submit transaction to Cardano blockchain. By default, it provides backend service implementations for Blockfrost, Koios etc.

But, if you want to use some other providers to build and submit transaction, you can easily do so by implementing following three simple interfaces :

  1. ProtocolParameterSupplier :- Implement this functional interface to provide current protocol parameters. Protocol parameters are required to build the transaction.
public interface ProtocolParamsSupplier {     ProtocolParams getProtocolParams(); }

2. UtxoSupplier:- This interface has only one method. Implement this interface to provide utxos which are required during transaction building.

3. TransactionProcessor :- Implement this interface to submit transaction to the Cardano blockchain.

public interface TransactionProcessor {     Result<String> submitTransaction(byte[] cborData) throws ApiException; }

Note: If you are using a supported backend provider like Blockfrost, you can just use the out-of-box backend service implementation.

Invoke Guess Sum Contract

In this section, we will go through the example step by step. You can also find the full source code of the example here.

Now, let’s start with some initialization steps.

Account setup

To submit any transaction to Cardano blockchain, we need an account to sign and pay transaction fee.

So, let’s see how to create a sender account object from a mnemonic phrase. We will be using this account to send fund to the script output and also to unlock the fund.

String senderMnemonic = "<24 words mnemonic phrase>";
Account sender = new Account(Networks.testnet(), senderMnemonic);
String senderAddress = sender.baseAddress();

Plutus V2 Script Object and Script address

We need to use the compiled version of our Sum contract script. The following code snippet shows how to initialize the PlutusV2Script instance and find the script address.

//Sum Script
PlutusV2Script sumScript =
          PlutusV2Script.builder()
                .cborHex("5907a65907a3010000323322323232323232323232323232323322323232323222232325335323232333573466e1ccc07000d200000201e01d3333573466e1cd55cea80224000466442466002006004646464646464646464646464646666ae68cdc39aab9d500c480008cccccccccccc88888888888848cccccccccccc00403403002c02802402001c01801401000c008cd405c060d5d0a80619a80b80c1aba1500b33501701935742a014666aa036eb94068d5d0a804999aa80dbae501a35742a01066a02e0446ae85401cccd5406c08dd69aba150063232323333573466e1cd55cea801240004664424660020060046464646666ae68cdc39aab9d5002480008cc8848cc00400c008cd40b5d69aba15002302e357426ae8940088c98c80c0cd5ce01881801709aab9e5001137540026ae854008c8c8c8cccd5cd19b8735573aa004900011991091980080180119a816bad35742a004605c6ae84d5d1280111931901819ab9c03103002e135573ca00226ea8004d5d09aba2500223263202c33573805a05805426aae7940044dd50009aba1500533501775c6ae854010ccd5406c07c8004d5d0a801999aa80dbae200135742a00460426ae84d5d1280111931901419ab9c029028026135744a00226ae8940044d5d1280089aba25001135744a00226ae8940044d5d1280089aba25001135744a00226ae8940044d55cf280089baa00135742a00860226ae84d5d1280211931900d19ab9c01b01a018375a00a6eb4014405c4c98c805ccd5ce24810350543500017135573ca00226ea800448c88c008dd6000990009aa80b911999aab9f0012500a233500930043574200460066ae880080508c8c8cccd5cd19b8735573aa004900011991091980080180118061aba150023005357426ae8940088c98c8050cd5ce00a80a00909aab9e5001137540024646464646666ae68cdc39aab9d5004480008cccc888848cccc00401401000c008c8c8c8cccd5cd19b8735573aa0049000119910919800801801180a9aba1500233500f014357426ae8940088c98c8064cd5ce00d00c80b89aab9e5001137540026ae854010ccd54021d728039aba150033232323333573466e1d4005200423212223002004357426aae79400c8cccd5cd19b875002480088c84888c004010dd71aba135573ca00846666ae68cdc3a801a400042444006464c6403666ae7007006c06406005c4d55cea80089baa00135742a00466a016eb8d5d09aba2500223263201533573802c02a02626ae8940044d5d1280089aab9e500113754002266aa002eb9d6889119118011bab00132001355014223233335573e0044a010466a00e66442466002006004600c6aae754008c014d55cf280118021aba200301213574200222440042442446600200800624464646666ae68cdc3a800a40004642446004006600a6ae84d55cf280191999ab9a3370ea0049001109100091931900819ab9c01101000e00d135573aa00226ea80048c8c8cccd5cd19b875001480188c848888c010014c01cd5d09aab9e500323333573466e1d400920042321222230020053009357426aae7940108cccd5cd19b875003480088c848888c004014c01cd5d09aab9e500523333573466e1d40112000232122223003005375c6ae84d55cf280311931900819ab9c01101000e00d00c00b135573aa00226ea80048c8c8cccd5cd19b8735573aa004900011991091980080180118029aba15002375a6ae84d5d1280111931900619ab9c00d00c00a135573ca00226ea80048c8cccd5cd19b8735573aa002900011bae357426aae7940088c98c8028cd5ce00580500409baa001232323232323333573466e1d4005200c21222222200323333573466e1d4009200a21222222200423333573466e1d400d2008233221222222233001009008375c6ae854014dd69aba135744a00a46666ae68cdc3a8022400c4664424444444660040120106eb8d5d0a8039bae357426ae89401c8cccd5cd19b875005480108cc8848888888cc018024020c030d5d0a8049bae357426ae8940248cccd5cd19b875006480088c848888888c01c020c034d5d09aab9e500b23333573466e1d401d2000232122222223005008300e357426aae7940308c98c804ccd5ce00a00980880800780700680600589aab9d5004135573ca00626aae7940084d55cf280089baa0012323232323333573466e1d400520022333222122333001005004003375a6ae854010dd69aba15003375a6ae84d5d1280191999ab9a3370ea0049000119091180100198041aba135573ca00c464c6401866ae700340300280244d55cea80189aba25001135573ca00226ea80048c8c8cccd5cd19b875001480088c8488c00400cdd71aba135573ca00646666ae68cdc3a8012400046424460040066eb8d5d09aab9e500423263200933573801401200e00c26aae7540044dd500089119191999ab9a3370ea00290021091100091999ab9a3370ea00490011190911180180218031aba135573ca00846666ae68cdc3a801a400042444004464c6401466ae7002c02802001c0184d55cea80089baa0012323333573466e1d40052002200923333573466e1d40092000200923263200633573800e00c00800626aae74dd5000a4c240029210350543100320013550032225335333573466e1c0092000005004100113300333702004900119b80002001122002122001112323001001223300330020020011")
String scriptAddress = AddressService.getInstance()
                              .getEntAddress(sumScript, Networks.testnet()).toBech32();

1. Create a reference script output

Now that we have an account and a script instance, we can build and submit a transaction to create a reference script output. The script body will be stored in the output on-chain.

Let’s first start with the expected output.

We want to create an output with script reference. For simplicity, we can set the lovelace amount in the output to 0. The library will automatically calculate the min required ada and set it accordingly. You can also see the sumScript is passed as parameter in scriptRef().

Output scriptRefOutput = Output.builder()
        .address(scriptAddress)
        .qty(BigInteger.ZERO) 
        .assetName(LOVELACE)
        .scriptRef(sumScript).build();

Now with the above expected output, let’s create an instance of TxBuilder using composable functions.

TxBuilder scriptRefTxBuilder = scriptRefOutput.outputBuilder()
        .buildInputs(InputBuilders.createFromSender(senderAddress, senderAddress))
        .andThen(BalanceTxBuilders.balanceTx(senderAddress,1));

In the above code snippet, we are getting TxOutputBuilder from the expected output (scriptRefOutput) and then using createFromSender composable function, we are creating TxInputBuilder and finally, buildInputs method is using TxInputBuilder to return TxBuilder.

So in summary, we selected required TransactionInputs for our expected output.

In Line-3, we are using another composable function “balanceTx” to balance the transaction. This is a wrapper function which calls other low level functions like feeCalculation, changeOutputAdjustment to balance an unbalanced transaction. The “balanceTx” function takes two parameters, change address and no of signers.

Now, we can build the transaction using TxBuilder and submit it to Cardano network.

Transaction signedTx = TxBuilderContext.init(utxoSupplier, protocolParamsSupplier)
                .buildAndSign(scriptRefTxBuilder, SignerProviders.signerFrom(sender));

Result<String> result = backendService.getTransactionService().submitTransaction(signedTx.serialize());
waitForTransactionHash(result)

In Line-1, a TxBuilderContext is created using UtxoSupplier and ProtocolParamsSupplier as parameters. If you are using Blockfrost or Koios as provider, these implementation are provided out-of-box. But you can easily provide your own supplier implementations if required.

In Line-2, TxBuilderContext’s buildAndSign method is used to build and sign the transaction by applying the previously created TxBuilder instance (scriptRefTxBuilder) and the TxSigner instance for sender.

In Line-4, the signed transaction is then submitted through the backend service’s TransactionService implementation. Alternatively, you can use your own TransactionProcessor implementation to submit the transaction.

That’s it. Now we have an output with script body as script reference.

2. Send fund to script address with inline datum

Now it’s time to send some fund to the script address that we had generated earlier using the PlutusV2Script instance. We will set an integer as datum value for the output. But instead of setting datum hash, the datum value will be directly stored in the output.

This is a simple transaction like step-1 but with inline datum.

PlutusData datum = BigIntPlutusData.of(8);

Output lockOutput = Output.builder()
                .address(scriptAddress)
                .assetName(LOVELACE)
                .qty(adaToLovelace(4))
                .datum(datum)
                .inlineDatum(true).build();

TxBuilder lockFundTxBuilder = lockOutput.outputBuilder()
                .buildInputs(InputBuilders.createFromSender(senderAddress, senderAddress))
                .andThen(BalanceTxBuilders.balanceTx(senderAddress, 1));

Transaction signedTx = TxBuilderContext.init(utxoSupplier, protocolParamsSupplier)
                .buildAndSign(lockFundTxBuilder, signerFrom(sender));

Result<String> result = backendService.getTransactionService().submitTransaction(signedTx.serialize());

In Line-1, we are creating an integer type Plutus data which is used as inline datum. The datum value is 8.

In Line-3 to Line-8, the expected output is defined with the inline datum (8).

Line-10 to Line 17 are similar to step-1, where a transaction is built and signed by TxBuilder, TxBuilderContext and finally submitted through TransactionService.

Now, we have an output at the script address with some locked fund (4 ADA) .

In the next section, we are going to create a spending transaction to unlock the fund.

3. Create a spending transaction (script txn) to spend the locked fund

To spend the locked fund, let’s first find the output at the script address.

You can use one of the helper method in ScriptUtxoFinders class to find the required output.

Utxo scriptUtxo = ScriptUtxoFinders.findFirstByDatumHashUsingDatum(utxoSupplier, scriptAddress, datum).orElseThrow();
BigInteger claimAmount = scriptUtxo
             .getAmount().stream().filter(amount -> LOVELACE.equals(amount.getUnit()))
             .findFirst()
             .orElseThrow().getQuantity();

In Line-1, we find the output using datum.

In Line-2 to Line-5, we are trying to find the claim amount from the output. It’s basically the lovelace value in this example.

Now, it’s time to find some collateral for our script transaction.

In Babbage era (after Vasil HF), we can also use an utxo with native tokens as collateral. The remaining lovelace amount and native tokens are sent back as collateral output. You can find more details about collateral outputs spec in CIP 40.

//Find collaterals
UtxoSelectionStrategy utxoSelectionStrategy = new LargestFirstUtxoSelectionStrategy(utxoSupplier);
Set<Utxo> collateralUtxos =
       utxoSelectionStrategy.select(senderAddress, new Amount(LOVELACE, adaToLovelace(5)), Collections.emptySet());

In the above code snippet, we are using UtxoSelectionStrategy to find an utxo with minimum 5 ADA for the collateral input. In this case, we are using LargestFirstUtxoSelectionStrategy implementation, but you can also use other implementations like DefaultUtxoSelectionStrategyImpl or RandomImproveSelectionStrategy.

Alternatively, you can also use DefaultUtxoSelector class by providing a predicate.

Now, let’s define the expected output and create a script call context.

Output output = Output.builder()
                .address(senderAddress)
                .assetName(LOVELACE)
                .qty(claimAmount)
                .build();

ScriptCallContext scriptCallContext = ScriptCallContext
                .builder()
                .script(sumScript)
                .exUnits(ExUnits.builder()  //Exact exUnits will be calculated later
                        .mem(BigInteger.valueOf(0))
                        .steps(BigInteger.valueOf(0))
                        .build())
                .redeemer(BigIntPlutusData.of(36))
                .redeemerTag(RedeemerTag.Spend).build();

Line-1 to Line-5, define an expected output with the claim amount and a receiver address.

In Line-7 to Line-8, we are creating a ScriptCallContext with all required data like script object, redeemer details and exunits.

Note that the redeemer value is set to 36 as the sum of our datum (0..8) is 36.
ExUnits value is set to 0 as we are going to evaluate the actual script cost later.

Also, we are not providing datum value as our script output has inline datum instead of datum hash.

Now we are ready to create our TxBuilder instance and then create & sign the transaction and finally submit to the Cardano network to unlock the locked value.

TxBuilder contractTxBuilder = output.outputBuilder()
                .buildInputs(InputBuilders.createFromUtxos(List.of(scriptUtxo)))
                .andThen(InputBuilders.referenceInputsFromUtxos(List.of(refScriptUtxo))) //CIP-31
                .andThen(CollateralBuilders.collateralOutputs(senderAddress, Lists.newArrayList(collateralUtxos))) //CIP-40
                .andThen(ScriptCallContextProviders.createFromScriptCallContext(scriptCallContext))
                .andThen((context, txn) -> {
                    //Calculate ExUnit. It should be done before balanceTx for accurate fee calculation
                    //update estimate ExUnits
                    ExUnits estimatedExUnits;
                    try {
                        estimatedExUnits = evaluateExUnits(txn);
                        txn.getWitnessSet().getRedeemers().get(0).setExUnits(estimatedExUnits);
                    } catch (Exception e) {
                        throw new ApiRuntimeException("Script cost evaluation failed", e);
                    }

                    //Remove script from witnessset as we are using reference script input
                    txn.getWitnessSet().getPlutusV2Scripts().clear();
                })
                .andThen(BalanceTxBuilders.balanceTx(senderAddress, 2));

TxBuilderContext txBuilderContext = TxBuilderContext.init(utxoSupplier, protocolParamsSupplier);
        
//Tx Build and Submit
TxSigner signer = signerFrom(sender);
Transaction signedTx = txBuilderContext.buildAndSign(contractTxBuilder, signer);
        
Result<String> result = backendService.getTransactionService().submitTransaction(signedTx.serialize());

Line-1 & Line-2 : Use the expected output and create inputs from script utxo that we found in one of the previous step.

Line-3 : Add reference script output (from step-1) as reference input. This is part of CIP 31.

Line-4 : Create collateral and collateral outputs using collateral utxos. The sender address is used as change address for collateral outputs.

Line-5: Use ScriptCallContext instance that we created before to populate script call related attributes in the transaction.

Line-6 to Line-15 : Evaluate exact ExUints by invoking evaluateTx() of backend service. This can also be done as a separate step before building of transaction. But, please make sure it’s done before fee calculation or before balanceTx method call.

Line-22 to Line-28 : Now as usual, build the transaction and submit it to Cardano network.

That’s it. We successfully invoked our on-chain plutus contract from off-chain Java code using Cardano Client Lib.

Full source code

For more examples, you can check Cardano Client Example GitHub repo.

Resources

  1. Cardano Client Lib GitHub Repo
  2. Cardano Client Example Repo
  3. New Composable functions to build transaction

Published on Java Code Geeks with permission by Satya Ranjan, partner at our JCG program. See the original article here: Call Plutus V2 contract from off-chain Java code using Cardano Client Lib

Opinions expressed by Java Code Geeks contributors are their own.

Satya

Satya is a solution architect and developer with more than 20 years of experience who has worked in a wide variety of organizations from product development, banking to IT consultancy. He is also a blockchain enthusiast.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments
Back to top button