[Caver-java] Dynamic ABI Loader

Tech at Klaytn
Klaytn
Published in
6 min readJun 17, 2021

See the list of articles here.
🇰🇷: [Caver-java] Dynamic ABI Loader

Caver-java’s Contract Dynamic ABI Loader allows you to easily deploy and execute contracts by loading Smart Contract ABI (Application Binary Interface) at runtime, and is included in caver.contract.Contract. With the Dynamic ABI Loader, you can load and execute various contracts with greater ease.

New Features

In the versions before caver-java v1.5.1, we had to use caver-java CLI to create a java file with the ABI file and binary data of a contract, and register it on the project. It was inconvenient that a new java file had to be created, registered and compiled, every time a contract changed.

But from caver-java v1.5.1 and onwards, the Dynamic ABI Loader feature has been added, so that the ABI file and binary data can be dynamically loaded. In the tutorial below, we will demonstrate how you can use it.

In each section, we will go through little snippets of the whole code; you can check the entire code in the link below.

The complete source code for caver-java

Creating Contract Instance

First, we will make a sample contract to load the ABI with caver-java. Name the file ‘KVstore.sol’ and enter the code below. This contract can query and retrieve key-value pairs.

// KVstore.sol
pragma solidity ^0.5.6;
contract KVstore {
mapping(string=>string) store;
constructor (string memory key, string memory value) public {
store[key] = value;
}
function get(string memory key) public view returns (string. memory) {
return store[key];
}
function set(string memory key, string memory value) public {
store[key] = value;
}
}

And then create the ABI and binary data using Solidity compiler.

solc --abi --bin ./KVStore.sol======= ./KVstore.sol:KVStore =======
Binary:
0x608060405234801561001057600080fd5b5060405161072d38038061072d8339810180604052604081101561003357600080fd5b81019080805164010000000081111561004b57600080fd5b8281019050602081018481111561006157600080fd5b815185600182028301116401000000008211171561007e57600080fd5b5050929190602001805164010000000081111561009a57600080fd5b828101905060208101848111156100b057600080fd5b81518560018202830111640100000000821117156100cd57600080fd5b5050929190505050806000836040518082805190602001908083835b6020831061010c57805182526020820191506020810190506020830392506100e9565b6001836020036101000a0380198251168184511680821785525050505050509050019150509081526020016040518091039020908051906020019061015292919061015a565b5050506101ff565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f1061019b57805160ff19168380011785556101c9565b828001600101855582156101c9579182015b828111156101c85782518255916020019190600101906101ad565b5b5090506101d691906101da565b5090565b6101fc91905b808211156101f85760008160009055506001016101e0565b5090565b90565b61051f8061020e6000396000f3fe608060405234801561001057600080fd5b50600436106100365760003560e01c8063693ec85e1461003b578063e942b5161461016f575b600080fd5b6100f46004803603602081101561005157600080fd5b810190808035906020019064010000000081111561006e57600080fd5b82018360208201111561008057600080fd5b803590602001918460018302840111640100000000831117156100a257600080fd5b91908080601f016020809104026020016040519081016040528093929190818152602001838380828437600081840152601f19601f8201169050808301925050505050505091929192905050506102c1565b6040518080602001828103825283818151815260200191508051906020019080838360005b83811015610134578082015181840152602081019050610119565b50505050905090810190601f1680156101615780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b6102bf6004803603604081101561018557600080fd5b81019080803590602001906401000000008111156101a257600080fd5b8201836020820111156101b457600080fd5b803590602001918460018302840111640100000000831117156101d657600080fd5b91908080601f016020809104026020016040519081016040528093929190818152602001838380828437600081840152601f19601f8201169050808301925050505050505091929192908035906020019064010000000081111561023957600080fd5b82018360208201111561024b57600080fd5b8035906020019184600183028401116401000000008311171561026d57600080fd5b91908080601f016020809104026020016040519081016040528093929190818152602001838380828437600081840152601f19601f8201169050808301925050505050505091929192905050506103cc565b005b60606000826040518082805190602001908083835b602083106102f957805182526020820191506020810190506020830392506102d6565b6001836020036101000a03801982511681845116808217855250505050505090500191505090815260200160405180910390208054600181600116156101000203166002900480601f0160208091040260200160405190810160405280929190818152602001828054600181600116156101000203166002900480156103c05780601f10610395576101008083540402835291602001916103c0565b820191906000526020600020905b8154815290600101906020018083116103a357829003601f168201915b50505050509050919050565b806000836040518082805190602001908083835b6020831061040357805182526020820191506020810190506020830392506103e0565b6001836020036101000a0380198251168184511680821785525050505050509050019150509081526020016040518091039020908051906020019061044992919061044e565b505050565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f1061048f57805160ff19168380011785556104bd565b828001600101855582156104bd579182015b828111156104bc5782518255916020019190600101906104a1565b5b5090506104ca91906104ce565b5090565b6104f091905b808211156104ec5760008160009055506001016104d4565b5090565b9056fea165627a7a72305820adabefbb9574a90843d986f100c723c37f37e79f289b16aa527705b5341499aa0029
Contract JSON ABI
[{"constant":true,"inputs":[{"name":"key","type":"string"}],"name":"get","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"key","type":"string"},{"name":"value","type":"string"}],"name":"set","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"inputs":[{"name":"key","type":"string"},{"name":"value","type":"string"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"}]

With the code below, you can create a contract instance directly from the ABI data using Dynamic ABI Loader, and check which functions of the contract can be called in the instance.

String abi = "[\n" +"  {\n" +
" \"constant\":true,\n" +
" \"inputs\":[\n" +
" {\n" +
" \"name\":\"key\",\n" +
" \"type\":\"string\"\n" +
" }\n" +
" ],\n" +
" \"name\":\"get\",\n" +
" \"outputs\":[\n" +
" {\n" +
" \"name\":\"\",\n" +
" \"type\":\"string\"\n" +
" }\n" +
" ],\n" +
" \"payable\":false,\n" +
" \"stateMutability\":\"view\",\n" +
" \"type\":\"function\"\n" +
" },\n" +
" {\n" +
" \"constant\":false,\n" +
" \"inputs\":[\n" +
" {\n" +
" \"name\":\"key\",\n" +
" \"type\":\"string\"\n" +
" },\n" +
" {\n" +
" \"name\":\"value\",\n" +
" \"type\":\"string\"\n" +
" }\n" +
" ],\n" +
" \"name\":\"set\",\n" +
" \"outputs\":[],\n" +
" \"payable\":false,\n" +
" \"stateMutability\":\"nonpayable\",\n" +
" \"type\":\"function\"\n" +
" },\n" +
" {\n" +
" \"inputs\":[\n" +
" {\n" +
" \"name\":\"key\",\n" +
" \"type\":\"string\"\n" +
" },\n" +
" {\n" +
" \"name\":\"value\",\n" +
" \"type\":\"string\"\n" +
" }\n" +
" ],\n" +
" \"payable\":false,\n" +
" \"stateMutability\":\"nonpayable\",\n" +
" \"type\":\"constructor\"\n" +
" }\n" +
"]";Contract contract = caver.contract.create(abi);
contract.getMethods().forEach((name, method) ->{
System.out.println(method.getType() + " " + caver.abi.buildFunctionString(method));
});

Execute the code, you will get this result:

function set(string,string)
function get(string)

Deploying Contracts

You can deploy contracts using binary data and ABI as shown below.

String abi = "Contract ABI Data";
String byteCode = "Contract Byte Code";
Contract contract = caver.contract.create(abi);
contract.getMethods().forEach((name, method) ->{
System.out.println(method.getType() + " " + caver.abi.buildFunctionString(method));
});
SingleKeyring deployerKeyring = caver.wallet.keyring.create(deployerAddress, deployerPrivateKey);
caver.wallet.add(deployerKeyring);

SendOptions sendOptions = new SendOptions(deployerKeyring.getAddress(), BigInteger.valueOf(650000));
Contract deployedContract = contract.deploy(sendOptions, byteCode, "Just", "Test");
System.out.println("Deployed address of contract: " + deployedContract.getContractAddress());

When executing the deploy function, the first parameter is a SendOptions object, which sets the transaction send options, and the second parameter is the contract binary data. After that, pass the parameters in the order as declared in the contract’s constructor. The deploy function creates an instance for the smart contract deploy transaction type.

SendOptions is an object for storing data required for creating transactions. SendOptions is defined as three fields: from, gas, and value, each playing a role as explained below:

  • from
    The value for the “from” field of the transaction instance. When signing the transaction, you can search for the keyring using this value in caver.wallet.
  • gas
    The value for the “gas” field of the transaction instance.
  • value
    The value for the “value” field of the transaction instance.

When you execute the above code, you will see in the result the contract address deployed on the Klaytn network.

0xc9267c4a39c606f4f90df4bb326ec1d0572d5750

Connecting to the Deployed Contract

In order to connect with the deployed contract, you need the ABI and address of the contract. Below is an example with which you can connect to a deployed contract.

String abi = "Contract ABI Data";
String contractAddress = "0xbe3867496bc619dff5467ceeb8d72779927dbe7a";
Contract contract = caver.contract.create(abi, contractAddress);
System.out.println(contract.getContractAddress());

When you run the code above, you will get a contract address as a result.

0xbe3867496bc619dff5467ceeb8d72779927dbe7a

Calling the Contract Function

The ABI data analyzed by Dynamic ABI Loader will be categorized according to type (Function or Event), and be created as an instance of ContractMethod and ContractEvent. The contract instance manages ContractMethod and ContractEvent objects as lists.

There are two ways to call a contract function.

  • call(): Receives data from a contract without changing the state of the contract. Internally, JSON-RPC klay_call() is used.
  • send(): Changes the state of the contract. Internally, Smart Contract Execution transaction will be used to send transactions to Klaytn.

Keep in mind these two ways for calling the contract function, so that you can use either call or send, depending on the function.

Contract function send()

Sending a contract function is to call a function that changes the state of the contract.

This send() function submits a Smart Contract Execution transaction to the network and returns a Transaction Receipt.

For the KVStore contract, created and deployed above, set() is the proper function using send(). Executing the below code will call the set() of the predeployed KVStore contract and get the Transaction Receipt returned.

String abi = "Contract ABI Data";
String contractAddress = "0xbe3867496bc619dff5467ceeb8d72779927dbe7a";
Contract contract = caver.contract.create(abi, contractAddress);

SingleKeyring executorKeyring = caver.wallet.keyring.create(executorAddress, executorPrivateKey);
caver.wallet.add(executorKeyring);

SendOptions sendOptions = new SendOptions(executorKeyring.getAddress(), BigInteger.valueOf(650000));
TransactionReceipt.TransactionReceiptData receiptData = contract.send(sendOptions, "set", "Just", "Test");
System.out.println(receiptData.getTransactionHash());

The first parameter for calling send() is SendOptions, and the second parameter is the name of the function to call. And then enter the necessary parameters in the right order. Send will create a SmartContractExecution transaction internally.

SendOptions is an object that stores the data necessary for creating transactions. SendOptions is defined with from, gas, and value, each playing a role as explained below:

  • from
    The value for the “from” field of the transaction instance. When signing the transaction, search for keyring with this value in caver.wallet.
  • gas
    The value for the “gas” field of the transaction instance.
  • value
    The value for the “value” field of the transaction instance.

The above code will return a transaction hash like the one below. Of course, since it’s just an example; the actual value will be different.

0x3d8dca23ca735e352444cba79dab16fe77ef3fc2121e42b1c1eedcfeb26eeed3

Contract function call()

Calling a contract function is essentially returning the data without changing its state.

Internally, this call() function will call klay_call of JSON-RPC API and obtain the corresponding data.

For the KVStore contract, created and deployed above, get() is the appropriate function using call(). Below is an example for calling the get() function and to get data.

String abi = "Contract ABI Data";
String contractAddress = "0xbe3867496bc619dff5467ceeb8d72779927dbe7a";
Contract contract = caver.contract.create(abi, contractAddress);

List<Type> returnedData = contract.call("get", "Just");
System.out.println((String)returnedData.get(0).getValue());

The result will be the instances of the classes that implement Solidity types. It will be expressed as a list.

The result of the above code will look like this:

Test

For more information on how to use smart contracts with caver-java, please refer to Getting Started in Klaytn Docs.

Thank you!

--

--