[Caver-java] Dynamic ABI Loader

Tech at Klaytn
Klaytn
Published in
22 min readNov 24, 2020

전체 포스팅 목록은 여기에서 확인하세요.

Contract dynamic ABI loader는 Smart Contract ABI(Application Binary Interface)를 런타임에 로딩하여 Contract 배포 및 실행을 쉽게 할 수 있게 해주는 기능으로, caver.contract.Contract에 포함되었습니다. 이 Dynamic ABI Loader를 통해 다양한 컨트랙트들을 좀 더 쉽게 로딩하고 실행할 수 있습니다.

새로운 점

caver-java 1.5.1 이전 버전에서는 caver-java console 프로그램을 이용하여, 사용하고자 하는 Contract의 abi파일 및 binary data를 넣어 해당하는 .java 파일을 생성 후 프로젝트에 등록해서 사용해야 했습니다. 이 경우, contract가 변경될 때마다 새로운 .java파일을 생성하고 등록하고 컴파일해야 하는 번거로움이 있습니다.

하지만 caver-java 1.5.1 부터는 abi파일과 binary data를 동적으로 로딩하여 사용할 수 있도록 Dynamic ABI loader 기능이 추가되었습니다. 아래 튜토리얼에서 사용 방법을 알아보도록 하겠습니다.

각 파트에서는 코드의 일부분을 설명하며, 전체 코드는 아래 링크에서 확인할 수 있습니다.

Dynamic ABI Loader를 통한 Contract 인스턴스 만들기

먼저, ABI를 caver-java로 로딩하기 위한 sample contract를 만들어 보겠습니다. ‘KVstore.sol’이라는 이름으로 파일을 만들고 아래 코드를 입력합니다. 이 컨트랙트는 key-value를 저장하고 읽어올 수 있는 기능을 가진 컨트랙트입니다.

// 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;
}
}

그리고 이 컨트랙트를 solidity compiler를 통해 abi와 binary data를 생성합니다.

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"}]

아래 코드와 같이 작성하면, JSON타입의 ABI data를 Dynamic ABI loader를 이용해서 Contract 인스턴스를 생성하고, 인스턴스 내 Contract의 어떤 function을 호출 할 수 있는지 확인해볼 수 있습니다.

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));
});

위 코드의 실행 결과는 아래와 같습니다.

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

Contract 배포하기

아래와 같이 binary data와 abi파일을 활용하여 contract를 배포할 수 있습니다.

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());

deploy 함수를 호출할 때, 첫 번째 파라미터로는 트랜잭션 전송 옵션을 설정하는 SendOptions 타입의 객체를, 두 번째 파라미터로는 컨트랙트 바이너리 데이터를 전달합니다. 그 이후부터는 컨트랙트의 생성자에 입력된 파라미터를 순서대로 전달합니다. deploy 함수는 내부적으로 smart contract deploy transaction type의 instance를 생성합니다.

SendOptions는 트랜잭션 생성에 필요한 정보를 저장하는 객체입니다. SendOptions은 from과 gas, value 총 3가지 필드로 정의되며, 각 필드는 아래와 같은 역할을 합니다.

  • from
    transaction instance의 “from”필드의 값으로 사용됩니다. transaction에 서명할 때 caver.wallet에서 이 값을 기준으로 keyring을 검색합니다.
  • gas
    transaction instance의 “gas”필드의 값으로 사용됩니다.
  • value
    transaction instance의 “value”필드의 값으로 사용됩니다.

위의 코드를 실행하면 아래와 같이 Klaytn 네트워크에 배포된 Contract의 address가 출력되는 것을 확인할 수 있습니다.

0xc9267c4a39c606f4f90df4bb326ec1d0572d5750

배포된 Contract 연결하기

이미 배포된 Contract와 연결하기 위해서는, 배포된 Contract의 ABI와 address가 필요합니다. 아래와 같이 코드를 작성하면 배포된 Contract와 연결할 수 있습니다.

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

위의 코드를 실행하면 아래와 같이 Contract address가 출력되는 것을 확인할 수 있습니다.

0xbe3867496bc619dff5467ceeb8d72779927dbe7a

Contract 함수 호출하기

Dynamic ABI Loader를 통해 분석된 ABI 정보들은 Type(Function, Event)에 따라 구분되어 ContractMethod와 ContractEvent의 instance로 만들어지고, Contract instance는 List 자료구조로 ContractMethod와 ContractEvent를 관리합니다.

Contract 함수를 호출하는 것은 2가지의 유형이 있습니다.

  • call() : Contract의 상태를 바꾸지 않고 Contract로부터 데이터를 전달 받음. 내부적으로는 JSON-RPC klay_call()을 사용함.
  • send() : Contract의 상태를 바꿈. 내부적으로는 Smart Contract Execution transaction을 사용하여 클레이튼에 트랜잭션을 전송함.

Contract 함수를 호출할 때 위 2가지의 타입이 있다는 것을 유의하고 Function의 성격에 맞게 call을 쓸 지, send를 쓸 지 선택하시면 됩니다.

Contract 함수 send()

Contract 함수를 send 한다는 것은 Contract의 상태를 바꾸는 행위를 하는 함수를 호출하는 것입니다.

이 send() 함수는 내부적으로 Smart Contract Execution transaction을 발생시키고 그에 따른 Transaction Receipt을 리턴합니다.

위에서 생성 및 배포한 KVStore contract에서는 set()이라는 함수가 이에 해당합니다. 아래와 같이 코드를 작성하면 미리 배포된 KVStore contract의 set()이라는 함수를 호출하고 Transaction Receipt을 리턴 받을 수 있습니다.

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());

send() 호출 시 첫 번째 파라미터로는 SendOptions를, 두 번째 파라미터로는 호출할 함수의 이름을 입력합니다. 이후 파라미터는 해당 함수 호출에 필요한 파라미터들을 순서대로 입력합니다. send는 내부적으로 SmartContractExecution transaction을 생성합니다.

SendOptions는 트랜잭션 생성에 필요한 정보를 저장하는 객체입니다. SendOptions은 from과 gas, value 총 3가지 필드로 정의되며, 각 필드는 아래와 같은 역할을 합니다.

  • from
    transaction instance의 “from”필드의 값으로 사용됩니다. transaction에 서명할 때 caver.wallet에서 이 값을 기준으로 keyring을 검색합니다.
  • gas
    transaction instance의 “gas”필드의 값으로 사용됩니다.
  • value
    transaction instance의 “value”필드의 값으로 사용됩니다.

위의 코드를 실행하면 transaction hash값이 출력됩니다. 아래의 값은 예시로, 실행할 때마다 트랜잭션 해시는 달라질 수 있습니다.

0x3d8dca23ca735e352444cba79dab16fe77ef3fc2121e42b1c1eedcfeb26eeed3

Contract 함수 call()

Contract함수를 call한다는 것은 Contract의 상태를 변화시키지 않고 Contract로 부터 데이터를 반환받는 성격의 함수를 호출하는 것입니다.

이 call()함수는 내부적으로 JSON-RPC의 klay_call API를 호출하고 그에 따른 data를 반환받게 됩니다.

위에서 생성 및 배포한 KVStore contract에서는 get()이라는 함수가 이에 해당됩니다. 아래와 같이 코드를 작성하면 Contract의 get()이라는 함수를 호출하고 get 함수가 반환하는 데이터를 전달 받을 수 있습니다.

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());

반환되는 값은 Solidity에서 정의된 type들을 구현한 class들 중 return type에 맞는 class instance의 값으로 매핑되며, 이는 List로 표현됩니다.

위와 같이 코드를 실행하면 아래와 같이 결과를 얻을 수 있습니다.

Test

caver-java로 Smart Contract를 다루는 더 자세한 내용은 Klaytn Docs의 Getting Started 문서를 통해 확인하실 수 있습니다. 감사합니다.

포스팅에 사용된 소스코드 링크

--

--