Getting Started (~v1.4.0)

Prerequisites

Dependency

maven

<dependency>
  <groupId>com.klaytn.caver</groupId>
  <artifactId>core</artifactId>
  <version>1.4.0</version>
</dependency>

gradle

implementation 'com.klaytn.caver:core:1.4.0'

If you want to use Android dependency, just append -android at the end of the version string. (e.g. 1.0.1-android)

If you want to see details of the JSON-RPC requests and responses, please include LOGBack dependency in your project. Below is a Gradle build file example. You can add the dependency to Maven as well. Since caver-java uses the SLF4J logging facade, you can switch to your preferred logging framework instead of LOGBack.

implementation "ch.qos.logback:logback-classic:1.2.3"

Note: In the central repository, the RC, Android, and Java versions are listed together. If you use wildcards to get a version, you may be using a version that is not appropriate for your platform.

Installation

If you want to generate transactions related with a smart contract, you need to install a Solidity compiler and caver-java command-line tool first.

Solidity Compiler

You can install the Solidity compiler locally, following the instructions as per the project documentation. Klaytn recommends you to install Solidity version either 0.4.24 or 0.5.6. If you are a macOS user, you can install the versions via Homebrew:

$ brew install klaytn/klaytn/solidity@0.4.24  # version 0.4.24
$ brew install klaytn/klaytn/solidity@0.5.6   # version 0.5.6

Command-line Tool

The command-line tool allows you to generate Solidity smart contract function wrappers from the command line.

Installation (Homebrew)

Java 1.8+ is required to install this.

$ brew tap klaytn/klaytn
$ brew install caver-java

After installation you can run command 'caver-java' like below:

$ caver-java solidity generate -b <smart-contract>.bin -a <smart-contract>.abi -o <outputPath> -p <packagePath>

Installation (Other)

Currently, we do not support other package managers. As another solution, we provide a method to build the CLI below.

  • Download or fork caver-java.

  • Do task 'shadowDistZip' in the console module using Gradle. As a result, console/build/distributions/console-shadow-{version}.zip is generated.

    $ ./gradlew :console:shadowDistZip
  • Unzip the zip file in the build directory

    $ unzip ./console/build/distributions/console-shadow-{version}.zip
  • Execute the binary file to run the command-line tool like below. You can find a shell script file for macOS users and a batch file for Window users.

    $ ./console/build/distributions/console-shadow-{version}/bin/caver-java

Managing Accounts

Creating an Account

In order to sign transactions, you need to have either an EC (Elliptic Curve) key pair or a Klaytn keystore file.

Using an EC Key Pair

You can create a Klaytn account using an EC key pair like below:

KlayCredentials credentials = KlayCredentials.create(Keys.createEcKeyPair());
String privateKey = Numeric.toHexStringWithPrefix(credentials.getEcKeyPair().getPrivateKey()); 
String address = credentials.getAddress();

Using a Keystore File

If you want to create a new account with a keystore file (you can also create a new keystore file in Klaytn Wallet):

KlayWalletUtils.generateNewWalletFile(
        <yourPassword>,
        new File(<walletFilePath>)
);

To load an account using a keystore file like below:

KlayCredentials credentials = KlayWalletUtils.loadCredentials(<password>, <walletFilePath>);

Sending a Transaction

Getting KLAY via Baobab Faucet

After creating an account, you can receive some Baobab testnet KLAY for the Baobab testnet via Baobab Faucet, available at https://baobab.wallet.klaytn.foundation/. The received testnet KLAY will be used for transaction fee later.

Connecting to Baobab

You can connect to the Baobab network like below:

Caver caver  = Caver.build(https://your.baobab.en.url:8651);

Sending a Value Transfer Transaction

After you get a Caver instance and create an account which has some KLAY, you can send 1 peb to a certain address(0xe97f27e9a5765ce36a7b919b1cb6004c7209217e) with a gas limit BigInteger.valueOf(100_000) like below:

TransactionManager is introduced to hide the complexity of transaction types. For example, a FeeDelegatedValueTransferTransaction object can be transformed from a ValueTransferTransaction object. For more details, see Fee Delegation. In addition to Fee Delegation, TransactionManager can be used with GetNonceProcessor, ErrorHandler, and TransactionReceiptProcessor.

TransactionManager transactionManager = new TransactionManager.Builder(caver, credentials)
        .setChaindId(ChainId.BAOBAB_TESTNET).build();

ValueTransferTransaction valueTransferTransaction = ValueTransferTransaction.create(
        credentials.getAddress(),  // fromAddress
        "0xe97f27e9a5765ce36a7b919b1cb6004c7209217e",  // toAddress
        BigInteger.ONE,  // value
        BigInteger.valueOf(100_000)  // gasLimit
);

KlayRawTransaction klayRawTransaction = transactionManager.sign(valueTransferTransaction);
String transactionHash = transactionManager.send(klayRawTransaction);

TransactionReceiptProcessor transactionReceiptProcessor = new PollingTransactionReceiptProcessor(caver, 1000, 15);  // pollingSleepDuration = 1000, pollingAttempts = 15
KlayTransactionReceipt.TransactionReceipt transactionReceipt = transactionReceiptProcessor.waitForTransactionReceipt(transactionHash);

If you use ValueTransfer class, you can more easily compose and send a transaction. This is because ValueTransfer class makes the processes above simple like below:

KlayTransactionReceipt.TransactionReceipt transactionReceipt
        = ValueTransfer.create(caver, credentials, ChainId.BAOBAB_TESTNET).sendFunds(
                redentials.getAddress(),  // fromAddress
                "0xe97f27e9a5765ce36a7b919b1cb6004c7209217e",  // toAddress
                BigDecimal.ONE,  // value 
                Convert.Unit.PEB,  // unit 
                BigInteger.valueOf(100_000)  // gasLimit
            ).send();

Checking Receipts

If you send a transaction via sendFunds, caver-java tries to get a transaction receipt by default. After you get a receipt, you can see the following log in the console.

{
   "jsonrpc":"2.0",
   "id":4,
   "result":{
      "blockHash":"0x45542cc3e3bce952f368c5da9d40f972c134fed2b2b6815231b5caf33c79dacd",
      "blockNumber":"0x39a57b",
      "contractAddress":null,
      "from":"0xe97f27e9a5765ce36a7b919b1cb6004c7209217e",
      "gas":"0x186a0",
      "gasPrice":"0x5d21dba00",
      "gasUsed":"0x5208",
      "logs":[],
      "logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
      "nonce":"0x114e",
      "senderTxHash":"0x3d50b9fa9fec58443f5618ed7e0f5aec5e9a6f7269d9ff606ff87156ca5b4afd",
      "signatures":[
         {
            ...
         }
      ],
      "status":"0x1",
      "to":"0xe97f27e9a5765ce36a7b919b1cb6004c7209217e",
      "transactionHash":"0x3d50b9fa9fec58443f5618ed7e0f5aec5e9a6f7269d9ff606ff87156ca5b4afd",
      "transactionIndex":"0x1",
      "type":"TxTypeValueTransfer",
      "typeInt":8,
      "value":"0x1"
   }
}

In this receipt, you can check the status of the transaction execution. If the 'status' field in the receipt is "0x1", it means the transaction is processed successfully. If not, the transaction failed. The detailed error message is presented in the txError field. For more detail, see txError.

Sending Other Transaction Types

Account Update

If you want to update the key of the given account to a new AccountKeyPublic key:

AccountUpdateTransaction accountUpdateTransaction = AccountUpdateTransaction.create(
        credentials.getAddress(),  // fromAddress
        AccountKeyPublic.create(
                "0xbf8154a3c1580b5478ceec0aac319055185280ce22406c6dc227f4de85316da1",  // publicKeyX
                "0x0dc8e4b9546adcc6d1f11796e43e478bd7ffbe302917667837179f4da77591d8"  // publicKeyY
        ),  // newAccountKey
        BigInteger.valueOf(100_000)  // gasLimit
);
Account.create(caver, credentials, ChainId.BAOBAB_TESTNET).sendUpdateTransaction(accountUpdateTransaction).send();

An account key represents the key structure associated with an account. To get more details and types about the Klaytn account key, please read AccountKey.

Smart Contract

caver-java supports auto-generation of smart contract wrapper code. Using the wrapper, you can easily deploy and execute a smart contract. Before generating a wrapper code, you need to compile the smart contract first. Note: This will only work if a Solidity compiler is installed in your computer. See Solidity Compiler.

$ solc <contract>.sol --bin --abi --optimize -o <output-dir>/

Then, generate the wrapper code using caver-java’s command-line tool.

$ caver-java solidity generate -b <smart-contract>.bin -a <smart-contract>.abi -o <outputPath> -p <packagePath>

Above command will output <smartContract>.java. After generating the wrapper code, you can deploy your smart contract like below:

<smartContract> contract = <smartContract>.deploy(
        caver, credentials, <chainId>, <gasProvider>,
        <param1>, ..., <paramN>).send();

After the smart contract has been deployed, you can create a smart contract instance like below:

<smartContract> contract = <smartContract>.load(
        <deployedContractAddress>, caver, credentials, <chainId>, <gasProvider>
);

To transact with a smart contract:

KlayTransactionReceipt.TransactionReceipt transactionReceipt = contract.<someMethod>(
        <param1>,
        ...).send();

To call a smart contract:

<type> result = contract.<someMethod>(<param1>, ...).send();

Example

This section describes how to deploy and execute a smart contract on the Baobab testnet. In this example, we use a smart contract ERC20Mock. If contract deployment fails and an empty contract address is returned, it will throw RuntimeException.

ERC20Mock erc20Mock = ERC20Mock.deploy(
        caver, credentials, 
        ChainId.BAOBAB_TESTNET,  // chainId
        new DefaultGasProvider(),  // gasProvider
        credentials.getAddress(),  // param1(initialAccount)
        BigInteger.valueOf(100)  // param2(initialBalance)
).send();
String deployedContractAddress = erc20Mock.getContractAddress();

To create an instance of the deployed ERC20Mock contract:

ERC20Mock erc20Mock = ERC20Mock.load(
        deployedContractAddress, 
        caver, credentials, 
        ChainId.BAOBAB_TESTNET,  // chainId 
        new DefaultGasProvider()  // gasProvider
);

If you transfer 10 tokens to a specified address (e.g., 0x2c8ad0ea2e0781db8b8c9242e07de3a5beabb71a), use the following code:

KlayTransactionReceipt.TransactionReceipt transactionReceipt = erc20Mock.transfer(
        "0x2c8ad0ea2e0781db8b8c9242e07de3a5beabb71a",  // toAddress
        BigInteger.valueOf(10)  // value
).send();

To check the balance of the recipient (e.g., 0x2c8ad0ea2e0781db8b8c9242e07de3a5beabb71a), use the code below:

BigInteger balance = erc20Mock.balanceOf(
        "0x2c8ad0ea2e0781db8b8c9242e07de3a5beabb71a"  // owner
).send();

Fee Delegation

Klaytn provides Fee Delegation feature which allows service providers to pay transaction fees instead of the users.

Value Transfer

On the client side, client who initiates the transaction will generate a fee-delegated value transfer transaction as follows: A sender creates a default ValueTransferTransaction object, then transactionManager.sign() returns a signed FeeDelegatedValueTransferTransaction object if the second parameter is set to true.

TransactionManager transactionManager = new TransactionManager.Builder(caver, credentials)
        .setChaindId(ChainId.BAOBAB_TESTNET).build();  // BAOBAB_TESTNET = 1001
ValueTransferTransaction valueTransferTransaction = ValueTransferTransaction.create(
        credentials.getAddress(),  // fromAddress
        "0xe97f27e9a5765ce36a7b919b1cb6004c7209217e",  // toAddress
        BigInteger.ONE,  // value
        BigInteger.valueOf(100_000)  // gasLimit
);
String senderRawTransaction = transactionManager.sign(valueTransferTransaction, true).getValueAsString();  // isFeeDelegated : true

A signed transaction, senderRawTransaction, is generated. Now the sender delivers the transaction to the fee payer who will pay for the transaction fee instead. Transferring transactions between the sender and the fee payer is not performed on the Klaytn network. The protocol should be defined by themselves.

After the fee payer gets the transaction from the sender, the fee payer can send the transaction using the FeePayerManager class as follows. FeePayerManager.executeTransaction() will sign the received transaction with the fee payer's private key and send the transaction to the Klaytn network.

KlayCredentials feePayer = KlayWalletUtils.loadCredentials(<password>, <walletfilePath>);
FeePayerManager feePayerManager = new FeePayerManager.Builder(caver, feePayer)
        .setChainId(ChainId.BAOBAB_TESTNET)
        .build();
feePayerManager.executeTransaction(senderRawTransaction);

Smart Contract Execution

The difference between fee-delegated smart contract execution and fee-delegated value transfer above is that this needs input data to call a function of a smart contract. A sender can generate a fee-delegated smart contract execution transaction as shown below. Note that transactionManager.sign() returns a TxTypeFeeDelegatedSmartContractExecution object if you pass true to the second parameter. The example below invokes the transfer method of ERC20Mock contract which is described in Smart Contract.

String recipient = "0x34f773c84fcf4a0a9e2ef07c4615601d60c3442f";
BigInteger transferValue = BigInteger.valueOf(20);
Function function = new Function(
        ERC20Mock.FUNC_TRANSFER,  // FUNC_TRANSFER = "transfer"
        Arrays.asList(new Address(recipient), new Uint256(transferValue)),  // inputParameters
        Collections.emptyList()  // outputParameters
);
String data = FunctionEncoder.encode(function);

TransactionManager transactionManager = new TransactionManager.Builder(caver, credentials)
        .setChaindId(ChainId.BAOBAB_TESTNET).build();  // BAOBAB_TESTNET = 1001
SmartContractExecutionTransaction smartContractExecution = 
        SmartContractExecutionTransaction.create(
                credentials.getAddress(),  // fromAddress
                erc20Mock.getContractAddress(),  // contractAddress
                BigInteger.ZERO,  // value
                Numeric.hexStringToByteArray(data),  // data
                BigInteger.valueOf(100_000)  // gasLimit
        );
String senderRawTransaction = transactionManager.sign(smartContractExecution, true).getValueAsString();

After you get senderRawTransaction, the rest of the process using FeePayerManager is the same way as you saw in fee-delegated value transfer above:

KlayCredentials feePayer = KlayWalletUtils.loadCredentials(<password>, <walletfilePath>);
FeePayerManager feePayerManager = new FeePayerManager.Builder(caver, feePayer).build();
feePayerManager.executeTransaction(senderRawTransaction);

Using various AccountKey Types

caver-java introduces new classes to support the various types of AccountKey supported by the platform. This feature is supported starting with version 1.2.0.

AccountKey

To update the account key on the Klaytn platform, caver-java provides the AccountKey interface. The following describes AccountKey implementations, AccountKeyPublic, AccountKeyWeightedMultiSig, and AccountKeyRoleBased. See Account Update for how to update an Account.

AccountKeyPublic

AccountKeyPublic is an implementation of AccountKey with one public key. You can create it like this:

ECKeyPair newKeyPair = Keys.createEcKeyPair();
AccountKeyPublic newAccountKey = AccountKeyPublic.create(newKeyPair.getPublicKey());

To use the account updated with AccountKeyPublic, you need to create KlayCredentials as follows:

KlayCredentials validCredentails = KlayCredentials.create(newKeyPair, oldCredentials.getAddress());

// Because the account address is decoupled from the AccountKeyPublic (public key), you can't use the account if you create the credentials without address as below.
KlayCredentials invalidCredentails = KlayCredentials.create(newKeyPair);

AccountKeyWeightedMultiSig

AccountKeyWeightedMultiSig is an account key that contains multiple public keys with varying weights. AccountKeyWeightedMultiSig also defines the threshold, the sum of the weights of the keys that must be signed in order to use the account. The maximum number of keys supported is 10. You can create AccountKeyWeightedMultiSig as below:

List<AccountKeyWeightedMultiSig.WeightedPublicKey> weightedTransactionPublicKeys = new ArrayList<>();

int weight1 = 10;
int weight2 = 30;

ECKeyPair ecKeyPair1 = Keys.createEcKeyPair();
ECKeyPair ecKeyPair2 = Keys.createEcKeyPair();

AccountKeyWeightedMultiSig.WeightedPublicKey weightedPublicKey1 = AccountKeyWeightedMultiSig.WeightedPublicKey.create(
				BigInteger.valueOf(weight1),
				AccountKeyPublic.create(ecKeyPair1.getPublicKey())
);

AccountKeyWeightedMultiSig.WeightedPublicKey weightedPublicKey2 = AccountKeyWeightedMultiSig.WeightedPublicKey.create(
				BigInteger.valueOf(weight2),
				AccountKeyPublic.create(ecKeyPair2.getPublicKey())
);

weightedTransactionPublicKeys.add(weightedPublicKey1);
weightedTransactionPublicKeys.add(weightedPublicKey2);

AccountKeyWeightedMultiSig newAccountKey = AccountKeyWeightedMultiSig.create(
                BigInteger.valueOf(weight1 + weight2),
                weightedTransactionPublicKeys
);

To use the account updated with AccountKeyWeightedMultiSig, you can create KlayCredentials as follows:

List<ECKeyPair> transactionECKeyPairList = new ArrayList<>();

transactionECKeyPairList.add(ecKeyPair1);
transactionECKeyPairList.add(ecKeyPair2);

KlayCredentials newCredentails = KlayCredentials.create(transactionECKeyPairList, address);

AccountKeyRoleBased

AccountKeyRoleBased is a list of AccountKey. Each AccountKey is assigned to a specific role according to its position. AccountKey can be AccountKeyPublic, AccountKeyWeightedMultiSig, or AccountKeyFail. If AccountKeyNil is used for a specific role, the key will not be updated for that role and the existing AccountKey will be used. If AccountKeyFail is used, signing for the role will fail always, so be careful using AccountKeyFail.

List<AccountKey> roleBasedAccountKeyList = new ArrayList<>();

ECKeyPair newKeyPair1 = Keys.createEcKeyPair(); // for RoleTransaction
roleBasedAccountKeyList.add(AccountKeyPublic.create(newKeyPair1.getPublicKey()));

ECKeyPair newKeyPair2 = Keys.createEcKeyPair(); // for RoleAccountUpdate
roleBasedAccountKeyList.add(AccountKeyPublic.create(newKeyPair2.getPublicKey()));

ECKeyPair newKeyPair3 = Keys.createEcKeyPair(); // for RoleFeePayer
roleBasedAccountKeyList.add(AccountKeyPublic.create(newKeyPair3.getPublicKey()));

newAccountKey = AccountKeyRoleBased.create(roleBasedAccountKeyList);

To use the account updated with AccountKeyRoleBased, you can create KlayCredentials as follows:

List<ECKeyPair> transactionECKeyPairList = Arrays.asList(newKeyPair1);
List<ECKeyPair> updateECKeyPairList = Arrays.asList(newKeyPair2);
List<ECKeyPair> feePayerECKeyPairList = Arrays.asList(newKeyPair3);

KlayCredentials newCredentails = KlayCredentials.create(transactionECKeyPairList, updateECKeyPairList, feePayerECKeyPairList, address);

If the account does not have a key for a specific role, pass an empty List as an argument.

List<ECKeyPair> transactionECKeyPairList = Collections.emptyList();
List<ECKeyPair> updateECKeyPairList = Arrays.asList(newKeyPair2);
List<ECKeyPair> feePayerECKeyPairList = Collections.emptyList();

KlayCredentials newCredentails = KlayCredentials.create(transactionECKeyPairList, updateECKeyPairList, feePayerECKeyPairList, address);

If the account has multiple keys for a specific role, you can pass the multiple keys as follows.

List<ECKeyPair> transactionECKeyPairList = Collections.emptyList();
List<ECKeyPair> updateECKeyPairList = Arrays.asList(newKeyPair2-1, newKeyPair2-2, newKeyPair2-3);
List<ECKeyPair> feePayerECKeyPairList = Collections.emptyList();

KlayCredentials newCredentails = KlayCredentials.create(transactionECKeyPairList, updateECKeyPairList, feePayerECKeyPairList, address);

Sending a Transaction with Multiple Signers

If an account has AccountKeyMultiSig or AccountKeyRoleBased, each key can be managed by different people.

This section describes how to collect signatures and send the transaction if there are multiple signers.

Sequential sender signing

The rawTransaction has an RLP encoded transaction that contains both txSignatures and feePayerSignatures. feePayerSignature is included only when the transaction is a fee delegated transaction.

In the absence of a fee payer, the process of repeatedly signing and executing a transaction can be divided into three parts. 1. RLP-encode the transaction and send it to the signer in the form of rawTransaction. 2. Signer signs with its own key for the received rawTransaction. 3. Sending the signed rawTransaction to EN. Step 2 can be repeated if there are multiple signers.

//// 1. Alice creates a transaction, signs it, and sends it to Bob.
//// Alice Side
ValueTransferTransaction transactionTransformer = ValueTransferTransaction.create(from, to, BigInteger.ONE, GAS_LIMIT);

TransactionManager transactionManager_alice = new TransactionManager.Builder(caver, senderCredential_alice)
                    .setTransactionReceiptProcessor(new PollingTransactionReceiptProcessor(caver, 1000, 10))
                    .setChaindId(LOCAL_CHAIN_ID)
                    .build();

String rawTransaction_signed_alice = transactionManager_alice.sign(transactionTransformer).getValueAsString();

//// 2. Bob signs the received transaction and sends it to Charlie.
//// Bob Side
            TransactionManager transactionManager_bob = new TransactionManager.Builder(caver, senderCredential_bob)
                    .setTransactionReceiptProcessor(new PollingTransactionReceiptProcessor(caver, 1000, 10))
                    .setChaindId(LOCAL_CHAIN_ID)
                    .build();

String rawTransaction_signed_alice_and_bob = transactionManager_bob.sign(rawTransaction_signed_alice).getValueAsString();

//// 3. Charlie signs the received transaction and sends it to Klaytn EN.
//// Charlie Side
TransactionManager transactionManager_charlie = new TransactionManager.Builder(caver, senderCredential_charlie)
                    .setTransactionReceiptProcessor(new PollingTransactionReceiptProcessor(caver, 1000, 10))
                    .setChaindId(LOCAL_CHAIN_ID)
                    .build();

KlayTransactionReceipt.TransactionReceipt transactionReceipt = transactionManager_charlie.executeTransaction(rawTransaction_signed_alice_and_bob);

Sequential fee-payer signing

Fee-payer signature(s) can also be added sequentially. Signing with FeePayerManager accumulates feePayerSignatures in the transaction. The signing order is not important. If you sign with TransactionManager, the txSignature is added. If you sign with FeePayerManger, the feePayerSignatures is added to the raw transaction.

//// 1. Bob receives a transaction from Alice and signs the transaction as a fee payer.
//// Bob Side
FeePayerManager feePayerManager_bob = new FeePayerManager.Builder(caver, feePayerCredentials_bob)
                    .setTransactionReceiptProcessor(new PollingTransactionReceiptProcessor(caver, 1000, 10))
                    .setChainId(LOCAL_CHAIN_ID)
                    .build();

String rawTransaction_signed_alice_and_bob = feePayerManager_bob.sign(rawTransaction_signed_alice).getValueAsString();

//// 2. Charlie signs the received transaction and sends it to Klaytn EN.
//// Charlie Side
FeePayerManager feePayerManager_charlie = new FeePayerManager.Builder(caver, feePayerCredentials_charlie)
                    .setTransactionReceiptProcessor(new PollingTransactionReceiptProcessor(caver, 1000, 10))
                    .setChainId(LOCAL_CHAIN_ID)
                    .build();

KlayTransactionReceipt.TransactionReceipt transactionReceipt =  feePayerManager_charlie.executeTransaction(rawTransaction_signed_alice_and_bob);

Thanks to

The web3j project for the inspiration. 🙂

Last updated