Intro to Blockchain With Ethereum, Web3j and Spring Boot: Smart Contracts
Want to learn more about how to implement smart contracts with blockchain? Check out this tutorial on how to implement smart contracts with Ethereum and Spring Boot.
Join the DZone community and get the full member experience.
Join For FreeI have already provided a quick introduction to building Spring Boot applications with Ethereum and web3j in one of my latest articles Introduction to Blockchain with Java using Ethereum, web3j, and Spring Boot. That article has attracted much interest from you, so I decided to describe some more advanced aspects related to Ethereum and web3j. Today, I’m going to show how you can implement Ethereum smart contracts in your application. First, let’s define what exactly is a smart contract.
The smart contract is just a program that is executed on EVM (Ethereum Virtual Machine). Each contract contains a collection of code (functions) and data. It has an address in the Ethereum blockchain that can interact with other contracts, make decisions, store data, and send ether to others. Ethereum smart contracts are usually written in a language named Solidity, which is a statically typed high-level language. Every contract needs to be compiled. After it, you can generate source code for your application based on the compiled binaries. Web3j library provides tools dedicated for that. Before we proceed to the source code, let’s discuss an architecture of our sample system.
It consists of two independent applications contract-service
and transaction-service
. The most business logic is performed by contract-service
application. It provides methods for creating smart wallets, deploying smart contracts on Ethereum, and calling contract’s functions. Application transaction-service
is responsible only for performing transaction between third-party and the owner of contract. It gets the owner’s account by calling endpoint exposed by contract-service
. Application contract-service
observing for transactions performed on the Ethereum node. If it is related to the contract owner’s account, the application calls the function responsible for transferring funds to contract receiver’s account on all contracts signed by this owner. Here’s the diagram that illustrates the process described above.
1. Building a Smart Contract With Solidity
The most popular tool for creating smart contracts in Ethereum is Solidity. Solidity is a contact-oriented, high-level language for implementing smart contracts. It was influenced by C++, Python, and JavaScript and is designed to target the Ethereum Virtual Machine (EVM). It is statically typed, supports inheritance, libraries and complex user-defined types among other features. For more information about that language, you should refer to Solidity documentation available on site http://solidity.readthedocs.io/.
Our main goal in this article is just to build a simple contract, compile it, and generate required source code. That’s why I don’t want to go into the exact implementation details of contracts using Solidity. Here’s the implementation of contract responsible for counting a fee for the incoming transaction. On the basis of this calculation, it deposits funds on the transaction owner’s account and withdraws funds from the sender’s account. This contract is signed between two users. Every one of them has it own smart wallet secured by their credentials. The understanding of this simple contract is very important, so let’s analyze it line after line.
Each contract is described by a percentage of the transaction, which goes to a receiver’s account (1) and the receiver’s account address (2). The two first lines of the contract declare variables for storing these parameters: fee
of Solidity type uint
, and receiver
of type address
. Both these values are initialized inside the contract’s constructor (5). Parameter fee
indicates the percentage fee of the transaction that is withdrawn from sender’s account and deposited on the receiver’s account. The line mapping (address => uint) public balances
maps addresses of all balances to unsigned integers (3). We have also defines event Sent, which is emitted after every transaction within the contract (4). Function getReceiverBalance
return the receiver’s account balance (6). Finally, there is a function sendTrx(...)
that can be can be called by an external client (7). It is responsible for performing withdrawal and deposit operations based on the contract’s percentage fee and transaction amount. It requires a little more attention. First, it needs to have payable
modifier to able to transfer funds between Ethereum accounts. After that, the transaction amount can be read from msg.value
parameter. Then, we call a function to send on receiver address variable with given amount in Wei and save this value on the contract’s balance. Additionally, we may send an event that can be received by a client application.
pragma solidity ^0.4.21;
contract TransactionFee {
// (1)
uint public fee;
// (2)
address public receiver;
// (3)
mapping (address => uint) public balances;
// (4)
event Sent(address from, address to, uint amount, bool sent);
// (5)
constructor(address _receiver, uint _fee) public {
receiver = _receiver;
fee = _fee;
}
// (6)
function getReceiverBalance() public view returns(uint) {
return receiver.balance;
}
// (7)
function sendTrx() public payable {
uint value = msg.value * fee / 100;
bool sent = receiver.send(value);
balances[receiver] += (value);
emit Sent(msg.sender, receiver, value, sent);
}
}
Once we have created a contract, we have to compile it and generate source code that can be used inside our application to deploy contract and call its functions. For just a quick check, you can use the Solidity compiler available online on site https://remix.ethereum.org.
2. Compiling Contract and Generating Source Code
Solidity provides up-to-date docker builds for their compiler. The released version is tagged with stable
, while unstable changes from the development branch are tagged with nightly
. However, that Docker image contains only compiler executable file, so we would have to mount a persistent volume with an input file with Solidity contract. Assuming that it is available under directory /home/docker
on our Docker machine, we can compile it using the following command. This command creates two files: a binary .bin
file, which is the smart contract code in a format the EVM can interpret, and an application binary interface .abi
file, which defines the smart contract methods.
$ docker run --rm -v /home/docker:/build ethereum/solc:stable /build/TransactionFee.sol --bin --abi --optimize -o /build
The compilation output files are available under /build
on the container and are persisted inside /home/docker
directory. The container is removed after compilation, because it is no needed now. We can generate source code from compiled contract using executable file provided together with Web3j library. It is available under directory ${WEB3J_HOME}/bin
. When generating source code using Web3j, we should pass location of .bin
and .abi
files, then set target package name and directory.
$ web3j solidity generate /build/transactionfee.bin /build/transactionfee.abi -p pl.piomin.services.contract.model -o src/main/java/
The web3j executable generates Java source file with Solidity contract name inside a given package. Here are the most important fragments of a generated source file.
public class Transactionfee extends Contract {
private static final String BINARY = "608060405234801561..."
public static final String FUNC_GETRECEIVERBALANCE = "getReceiverBalance";
public static final String FUNC_BALANCES = "balances";
public static final String FUNC_SENDTRX = "sendTrx";
public static final String FUNC_FEE = "fee";
public static final String FUNC_RECEIVER = "receiver";
// ...
protected Transactionfee(String contractAddress, Web3j web3j, TransactionManager transactionManager, BigInteger gasPrice, BigInteger gasLimit) {
super(BINARY, contractAddress, web3j, transactionManager, gasPrice, gasLimit);
}
public RemoteCall getReceiverBalance() {
final Function function = new Function(FUNC_GETRECEIVERBALANCE,
Arrays.asList(),
Arrays.asList(new TypeReference() {}));
return executeRemoteCallSingleValueReturn(function, BigInteger.class);
}
public RemoteCall balances(String param0) {
final Function function = new Function(FUNC_BALANCES,
Arrays.asList(new org.web3j.abi.datatypes.Address(param0)),
Arrays.asList(new TypeReference() {}));
return executeRemoteCallSingleValueReturn(function, BigInteger.class);
}
public RemoteCall sendTrx(BigInteger weiValue) {
final Function function = new Function(
FUNC_SENDTRX,
Arrays.asList(),
Collections.emptyList());
return executeRemoteCallTransaction(function, weiValue);
}
public RemoteCall fee() {
final Function function = new Function(FUNC_FEE,
Arrays.asList(),
Arrays.asList(new TypeReference() {}));
return executeRemoteCallSingleValueReturn(function, BigInteger.class);
}
public RemoteCall receiver() {
final Function function = new Function(FUNC_RECEIVER,
Arrays.asList(),
Arrays.<TypeReference>asList(new TypeReference
<Address>() {}));
return executeRemoteCallSingleValueReturn(function, String.class);
}
public static RemoteCall deploy(Web3j web3j, Credentials credentials, BigInteger gasPrice, BigInteger gasLimit, String _receiver, BigInteger _fee) {
String encodedConstructor = FunctionEncoder.encodeConstructor(Arrays.asList(new org.web3j.abi.datatypes.Address(_receiver),
new org.web3j.abi.datatypes.generated.Uint256(_fee)));
return deployRemoteCall(Transactionfee.class, web3j, credentials, gasPrice, gasLimit, BINARY, encodedConstructor);
}
public static RemoteCall deploy(Web3j web3j, TransactionManager transactionManager, BigInteger gasPrice, BigInteger gasLimit, String _receiver, BigInteger _fee) {
String encodedConstructor = FunctionEncoder.encodeConstructor(Arrays.asList(new org.web3j.abi.datatypes.Address(_receiver),
new org.web3j.abi.datatypes.generated.Uint256(_fee)));
return deployRemoteCall(Transactionfee.class, web3j, transactionManager, gasPrice, gasLimit, BINARY, encodedConstructor);
}
// ...
public Observable sentEventObservable(DefaultBlockParameter startBlock, DefaultBlockParameter endBlock) {
EthFilter filter = new EthFilter(startBlock, endBlock, getContractAddress());
filter.addSingleTopic(EventEncoder.encode(SENT_EVENT));
return sentEventObservable(filter);
}
public static Transactionfee load(String contractAddress, Web3j web3j, Credentials credentials, BigInteger gasPrice, BigInteger gasLimit) {
return new Transactionfee(contractAddress, web3j, credentials, gasPrice, gasLimit);
}
public static Transactionfee load(String contractAddress, Web3j web3j, TransactionManager transactionManager, BigInteger gasPrice, BigInteger gasLimit) {
return new Transactionfee(contractAddress, web3j, transactionManager, gasPrice, gasLimit);
}
public static class SentEventResponse {
public Log log;
public String from;
public String to;
public BigInteger amount;
public Boolean sent;
}
}
3. Deploying Contract
Once we have successfully generated a Java object representing contract inside our application, we may proceed to the application development. We will begin from contract-service
. First, we will create a smart wallet with credentials and sufficient funds for signing contracts as an owner. The following fragment of code is responsible for that and is invoked just after application boot. You can also see here an implementation of HTTP GET method responsible for returning owner account address.
@PostConstruct
public void init() throws IOException, CipherException, NoSuchAlgorithmException, NoSuchProviderException, InvalidAlgorithmParameterException {
String file = WalletUtils.generateLightNewWalletFile("piot123", null);
credentials = WalletUtils.loadCredentials("piot123", file);
LOGGER.info("Credentials created: file={}, address={}", file, credentials.getAddress());
EthCoinbase coinbase = web3j.ethCoinbase().send();
EthGetTransactionCount transactionCount = web3j.ethGetTransactionCount(coinbase.getAddress(), DefaultBlockParameterName.LATEST).send();
Transaction transaction = Transaction.createEtherTransaction(coinbase.getAddress(), transactionCount.getTransactionCount(), BigInteger.valueOf(20_000_000_000L), BigInteger.valueOf(21_000), credentials.getAddress(),BigInteger.valueOf(25_000_000_000_000_000L));
web3j.ethSendTransaction(transaction).send();
EthGetBalance balance = web3j.ethGetBalance(credentials.getAddress(), DefaultBlockParameterName.LATEST).send();
LOGGER.info("Balance: {}", balance.getBalance().longValue());
}
@GetMapping("/owner")
public String getOwnerAccount() {
return credentials.getAddress();
}
Application contract-service
exposes some endpoints that can be called by an external client or the second application in our sample system – transaction-service
. The following implementation of POST /contract
method performs two actions. First, it creates a new smart wallet with credentials. Then, it uses those credentials to sign a smart contract with the address defined in the previous step. To sign a new contract, you have to call method deploy
from class generated from the Solidity definition – Transactionfee
. It is responsible for deploying a new instance of the contract on the Ethereum node.
private List contracts = new ArrayList();
@PostMapping
public Contract createContract(@RequestBody Contract newContract) throws Exception {
String file = WalletUtils.generateLightNewWalletFile("piot123", null);
Credentials receiverCredentials = WalletUtils.loadCredentials("piot123", file);
LOGGER.info("Credentials created: file={}, address={}", file, credentials.getAddress());
Transactionfee2 contract = Transactionfee2.deploy(web3j, credentials, GAS_PRICE, GAS_LIMIT, receiverCredentials.getAddress(), BigInteger.valueOf(newContract.getFee())).send();
newContract.setReceiver(receiverCredentials.getAddress());
newContract.setAddress(contract.getContractAddress());
contracts.add(contract.getContractAddress());
LOGGER.info("New contract deployed: address={}", contract.getContractAddress());
Optional tr = contract.getTransactionReceipt();
if (tr.isPresent()) {
LOGGER.info("Transaction receipt: from={}, to={}, gas={}", tr.get().getFrom(), tr.get().getTo(), tr.get().getGasUsed().intValue());
}
return newContract;
}
Every contract deployed on Ethereum has its own unique address. The unique address of every created contract is stored by the application. Then, the application is able to load all existing contracts using those addresses. The following method is responsible for executing method sentTrx
on the selected contract.
public void processContracts(long transactionAmount) {
contracts.forEach(it -> {
Transactionfee contract = Transactionfee.load(it, web3j, credentials, GAS_PRICE, GAS_LIMIT);
try {
TransactionReceipt tr = contract.sendTrx(BigInteger.valueOf(transactionAmount)).send();
LOGGER.info("Transaction receipt: from={}, to={}, gas={}", tr.getFrom(), tr.getTo(), tr.getGasUsed().intValue());
LOGGER.info("Get receiver: {}", contract.getReceiverBalance().send().longValue());
EthFilter filter = new EthFilter(DefaultBlockParameterName.EARLIEST, DefaultBlockParameterName.LATEST, contract.getContractAddress());
web3j.ethLogObservable(filter).subscribe(log -> {
LOGGER.info("Log: {}", log.getData());
});
} catch (Exception e) {
LOGGER.error("Error during contract execution", e);
}
});
}
Application contract-service
listens for transactions incoming to the Ethereum node that has been sent by transaction-service
. If a target account of the transaction is equal to the contract's owner account, a given transaction is processed.
@Autowired
Web3j web3j;
@Autowired
ContractService service;
@PostConstruct
public void listen() {
web3j.transactionObservable().subscribe(tx -> {
if (tx.getTo() != null && tx.getTo().equals(service.getOwnerAccount())) {
LOGGER.info("New tx: id={}, block={}, from={}, to={}, value={}", tx.getHash(), tx.getBlockHash(), tx.getFrom(), tx.getTo(), tx.getValue().intValue());
service.processContracts(tx.getValue().longValue());
} else {
LOGGER.info("Not matched: id={}, to={}", tx.getHash(), tx.getTo());
}
});
}
Here’s the source code from transaction-service
responsible for transfer funds from a third-party account to the contract's owner account.
@Value("${contract-service.url}")
String url;
@Autowired
Web3j web3j;
@Autowired
RestTemplate template;
Credentials credentials;
@PostMapping
public String performTransaction(@RequestBody TransactionRequest request) throws Exception {
EthAccounts accounts = web3j.ethAccounts().send();
String owner = template.getForObject(url, String.class);
EthGetTransactionCount transactionCount = web3j.ethGetTransactionCount(accounts.getAccounts().get(request.getFromId()), DefaultBlockParameterName.LATEST).send();
Transaction transaction = Transaction.createEtherTransaction(accounts.getAccounts().get(request.getFromId()), transactionCount.getTransactionCount(), GAS_PRICE, GAS_LIMIT, owner, BigInteger.valueOf(request.getAmount()));
EthSendTransaction response = web3j.ethSendTransaction(transaction).send();
if (response.getError() != null) {
LOGGER.error("Transaction error: {}", response.getError().getMessage());
return "ERR";
}
LOGGER.info("Transaction: {}", response.getResult());
EthGetTransactionReceipt receipt = web3j.ethGetTransactionReceipt(response.getTransactionHash()).send();
if (receipt.getTransactionReceipt().isPresent()) {
TransactionReceipt r = receipt.getTransactionReceipt().get();
LOGGER.info("Tx receipt: from={}, to={}, gas={}, cumulativeGas={}", r.getFrom(), r.getTo(), r.getGasUsed().intValue(), r.getCumulativeGasUsed().intValue());
}
EthGetBalance balance = web3j.ethGetBalance(accounts.getAccounts().get(request.getFromId()), DefaultBlockParameterName.LATEST).send();
LOGGER.info("Balance: address={}, amount={}", accounts.getAccounts().get(request.getFromId()), balance.getBalance().longValue());
balance = web3j.ethGetBalance(owner, DefaultBlockParameterName.LATEST).send();
LOGGER.info("Balance: address={}, amount={}", owner, balance.getBalance().longValue());
return response.getTransactionHash();
}
4. Test Scenario
To run the test scenario, we need to have launched:
- Ethereum node in development on Docker container
- Ethereum Geth console client on Docker container
- The instance of
contact-service
application, by default available on port 8090 - The instance of
transaction-service
application, by default available on port 8091
Instruction on how to run the Ethereum node and Geth client using Docker container is available in my previous article about blockchain, Introduction to Blockchain with Java using Ethereum, web3j and Spring Boot.
Before starting the sample applications, we should create at least one test account on Ethereum node. To achieve it, we have to execute the personal.newAccount
Geth command, as shown below.
After the startup application transaction-service,
you can transfer some funds from a coinbase account to all other existing accounts.
The next step is to create some contracts using the owner account created automatically by contract-service
on startup. You should call the POST /contract
method with the fee
parameter that specifies the percentage of a transaction amount transfer from the contract owner’s account to the contract receiver’s account. Using the following command, I have deployed two contracts with 10 percent and 5 percent. It means that 10 percent and 5 percent of each transaction sent to owner’s account by third-party users is transferred to the accounts generated by POST method. The address of account created by the POST method is returned in the response in the receiver
field.
curl -X POST -H "Content-Type: application/json" -d '{"fee":10}' http://localhost:8090/contract
{"fee": 10,"receiver": "0x864ef9931c2690efcc6a773760237c4b09f40e65","address": "0xa6205a746ae0858fa22d6451b794cc977faa507c"}
curl -X POST -H "Content-Type: application/json" -d '{"fee":5}' http://localhost:8090/contract
{"fee": 5,"receiver": "0x098898594d7acd1481324af779e431ab87a3155d","address": "0x9c64d6b0fc01ee055e114a528fb5ad853843cde3"}
If contracts have been successfully deployed, the last thing to do is to send a transaction by calling endpoint POST /transaction
exposed by transaction-service
. The owner account is automatically retrieved from contract-service
. You have to set the transaction amount and source account index (means eth.accounts[index]
).
curl -X POST -H "Content-Type: application/json" -d '{"amount":1000000,"fromId":1}' http://localhost:8090/transaction
Ok, that’s it! Now, the transaction is received by contract-service
, which executes the function sendTrx(...)
on all defined contracts. As a result, 10 percent and 5 percent of that transaction amount goes to contract receivers.
The sample applications source code is available in repository sample-spring-blockchain-contract (https://github.com/piomin/sample-spring-blockchain-contract.git). Enjoy!
Published at DZone with permission of Piotr Mińkowski, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments