30-Aug-2020 | Like this? Dislike this? Let me know |
Updated 24-Aug-2021 to reflect changes: epirus from 1.4.0 to 1.4.1 (fixes weird extra "generate" verb)
Updated 8-Apr-2023 to reflect changes: epirus is back to web3j and v1.4.2; geth and solc have later revs AND locations
Smart contracts and Ethereum are all the rage. So how can you write and run a Hello World in solidity, the defacto language that compiles to the Ethereum virtual machine (EVM), and call it from a Java client? This is not about transferring ETH from one wallet to another. That is ... "easy". We're talking about experimenting with smart contract call types, return types, and storage dynamics in solidity and interacting with it in a Java client.
Turns out it is a harder that you think -- mostly because the existing documentation on this topic (and there is a lot of it) starts with you doing some of the following:
The key here is to use geth and web3j. Here's how -- from scratch. It will take all of 5 minutes. And most important, you will understand why what utils and execs and libraries are being used. This example has no hidden magic; only what you download and run is used. In particular, no maven transitive closure on dependencies is used.
$ cd $HOME/opt $ tar xf /tmp/what-you-downloaded.tar $ ls -l ./geth-linux-amd64-1.9.20-979fc968 ./geth-linux-amd64-1.9.20-979fc968/COPYING ./geth-linux-amd64-1.9.20-979fc968/geth $ ln -s ./geth-linux-amd64-1.9.20-979fc968/geth $HOME/bin $ geth version Geth Version: 1.11.5-stable Git Commit: a38f4108571d1a144dc3cf3faf8990430d109bc4 Git Commit Date: 20230321 Architecture: amd64 Go Version: go1.20.2 Operating System: darwin GOPATH= GOROOT=
$ cd $HOME $ mkdir -p eth/dev/data
$ cd $HOME/eth/dev # NOT in the data directory; one above it $ geth --dev --datadir data --http --http.api admin,debug,eth,miner,net,personal,shh,txpool,web3
If later on when deploying a contract or calling a state-changing function you get an error similar to "Error processing transaction request: only replay-protected (EIP-155) transactions allowed over RPC" then add
--rpc.allow-unprotected-txs
$ cd eth/dev $ nohup geth --dev --datadir data --http --http.api admin,debug,eth,miner,net,personal,shh,txpool,web3 >> $HOME/geth.log 2>&1 &
$ curl -L get.epirus.io | sh
After you do this, a set of libs will be installed in $HOME/.epirus and your PATH will be expanded to include $HOME/.epirus. You can tell if the install worked by executing this:
$ epirus ______ _ | ____| (_) | |__ _ __ _ _ __ _ _ ___ | __| | '_ \| | '__| | | / __| | |____| |_) | | | | |_| \__ \ |______| .__/|_|_| \__,_|___/ | | |_| epirus [OPTIONS] [COMMAND] Description: ... more
$ ls -l solc-static-linux -rw-r--r--@ 1 swguy staff 11346488 Dec 11 16:11 solc-static-linux $ mkdir -p ~/opt/solc-0.8.0 $ mv solc-static-linux ~/opt/solc-0.8.0 $ ln -s ~/opt/solc-0.8.0/solc-static-linux $HOME/bin/solc $ ls ~/bin lrwxrwxrwx 1 swguy staff 55 Aug 30 22:26 geth -> /home/swguy/opt/geth-linux-amd64-1.9.20-979fc968/geth lrwxrwxrwx 1 swguy staff 55 Aug 30 22:26 solc -> /home/swguy/opt/solc-0.8.0/solc-static-linux $ solc --version solc, the solidity compiler commandline interface Version: 0.8.0+commit.9e61f92b.Linux.g++
$ cd $HOME $ mkdir -p projects/myroot # our home base $ cd $HOME/projects/myroot $ mkdir lib # This will simplify classpaths later on $ # Symlink the big web3j jar into our lib $ ln -s ~/.epirus/epirus-cli-shadow-1.4.0/lib/epirus-cli-1.4.0-all.jar lib # if the 'epirus' exec worked before then so will this $ mkdir -p src/main/sol/contracts # where the Hello World smart contract goes $ mkdir -p src/main/java/com/yourcompany/web3/apps # where the Hello World java caller goes
pragma solidity ^0.7.0; // SPDX-License-Identifier: MIT contract TheContract { // Some contract-specific data. struct Loan { bytes cparty; uint32 amount; } Loan loan; // "autoconstructed" constructor(bytes memory cparty, uint32 amount) { loan.cparty = cparty; loan.amount = amount; } /* Things that generate state change cannot return data. They only return * a TransactionReceipt object. Only view or pure functions will be * wrapped with nice type specific return values. Even if you declare * this function to return something, it is IGNORED in the Java wrapper. * You will NOT get * the types back in the Java-wrapped function; you have to wait until the * state change is committed to the block and then you can ask for the data * with a view scoped function. */ function changeCounterparty(string calldata newCpty) public { loan.cparty = bytes(newCpty); } // Returning a string instead of bytes means in Java you get a String // instead of byte[] which is friendlier. function getCounterparty() public view returns (string memory) { return string(loan.cparty); } // An example of a function that returns two types function getInfo() public view returns (bytes memory, uint amount) { return (loan.cparty, loan.amount); } // structs e.g. Loan are internal to contracts. You can pass and receive // structs from an internal function, but you cannot do so to the outside // world i.e. there is no Java wrapper for Loan. }
$ cd $HOME/projects/myroot $ solc --abi --bin --overwrite -o build/main src/main/sol/contracts/TheContract.sol Compiler run successful. Artifact(s) can be found in directory build/main.
$ cd $HOME/projects/myroot $ epirus generate solidity --binFile=build/main/TheContract.bin --abiFile=build/main/TheContract.abi --outputDir=src/main/java --package=com.yourcompany.web3.generated
// deploy1.java package com.yourcompany.web3.apps; // Super important: Our app uses the generated Java from solc! import com.yourcompany.web3.generated.TheContract; import java.math.BigInteger; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.web3j.crypto.Credentials; import org.web3j.crypto.WalletUtils; import org.web3j.protocol.Web3j; import org.web3j.protocol.http.HttpService; import org.web3j.tx.Contract; // for GAS_LIMIT constant import org.web3j.tx.ManagedTransaction; // GAS_PRICE constant public class deploy1 { private static final Logger log = LoggerFactory.getLogger(deploy1.class); public static void main(String[] args) throws Exception { new deploy1().run(); } private void run() { try { // This is the geth dev server you are running. Note: NOT https! String MY_CONN_URL = "http://127.0.0.1:8545"; // This is the developer wallet autocreated and funded by the --dev option // when we started geth. // There should be only one file in the keystore directory; use that one. // The actual name will be different than this. Again, the point of using // --datadir with the --dev option is that this resource will be created // just once and you don't have to keep finding a new wallet file and // change the program: String MY_WALLET_FILE = "yourhome/eth/dev/data/keystore/UTC--2020-08-30T15-35-42.179527700Z--ad2a9bde6c3fe0149c29db01ed40966b1b06f3d0"; String MY_WALLET_PWD = ""; // IMPORTANT! The password for the geth --dev developer account wallet is BLANK! System.out.println("Connecting to " + MY_CONN_URL); Web3j web3j = Web3j.build(new HttpService(MY_CONN_URL)); System.out.println("Loading credentials..."); Credentials credentials = WalletUtils.loadCredentials(MY_WALLET_PWD, MY_WALLET_FILE); System.out.println("Deploying smart contract; be patient..."); TheContract contract = TheContract.deploy( // These first 4 args are always the same types: web3j, credentials, ManagedTransaction.GAS_PRICE, Contract.GAS_LIMIT, // The next are the required args for the constructor() of the smart contract! // OR no additional args if the constructor has no args. "CPTY1".getBytes(), // notice: we pass byte[], not String new BigInteger("1000") // ... and ALL uint types go in (and out) as BigInteger ).send(); String contractAddress = contract.getContractAddress(); System.out.println("Contract address: " + contractAddress); web3j.shutdown(); } catch(Exception e) { System.out.println("exception: " + e); } } }
// access1.java package com.yourcompany.web3.apps; // Super important: Our app uses the generated Java from solc! import com.yourcompany.web3.generated.TheContract; import java.math.BigInteger; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.web3j.crypto.Credentials; import org.web3j.crypto.WalletUtils; import org.web3j.protocol.Web3j; import org.web3j.protocol.core.methods.response.TransactionReceipt; import org.web3j.protocol.http.HttpService; import org.web3j.tx.Contract; // for GAS_LIMIT constant import org.web3j.tx.ManagedTransaction; // GAS_PRICE constant import org.web3j.tx.Transfer; import org.web3j.utils.Convert; import org.web3j.utils.Numeric; import org.web3j.tuples.generated.Tuple2; public class access1 { private static final Logger log = LoggerFactory.getLogger(access1.class); public static void main(String[] args) throws Exception { new access1().run(args); } private void run(String[] args) { String contractAddr = args[0]; try { String MY_CONN_URL = "http://127.0.0.1:8545"; String MY_WALLET_FILE = "yourhome/eth/dev/data/keystore/UTC--2020-08-30T15-35-42.179527700Z--ad2a9bde6c3fe0149c29db01ed40966b1b06f3d0"; String MY_WALLET_PWD = ""; System.out.println("Connecting to " + MY_CONN_URL); Web3j web3j = Web3j.build(new HttpService(MY_CONN_URL)); System.out.println("Loading credentials..."); Credentials credentials = WalletUtils.loadCredentials(MY_WALLET_PWD, MY_WALLET_FILE); TheContract ct2 = TheContract.load(contractAddr, web3j, credentials, ManagedTransaction.GAS_PRICE, Contract.GAS_LIMIT); String cpty = ct2.getCounterparty().send(); System.out.println("cpty: " + cpty); // State changing function do NOT return regular types, only a // TransactionReceipt TransactionReceipt TXr = ct2.changeCounterparty("FOO").send(); System.out.println(" TX status: " + TXr.getStatus()); System.out.println(" TX root: " + TXr.getRoot()); System.out.println(" TX TXhash: " + TXr.getTransactionHash()); System.out.println(" TX blk hash: " + TXr.getBlockHash()); System.out.println(" TX blk num: " + TXr.getBlockNumber()); System.out.println(" TX gas used: " + TXr.getGasUsed()); // this is particularly cool System.out.println(" TX from: " + TXr.getFrom()); System.out.println(" TX to: " + TXr.getTo()); // Thanks to uint256, all integers coming out of smart contracts use // BigInteger, not int or long. And you cannot communicate with structs, // only basic types. The org.web3j.tuples.generated package provides // up to 20 returns (Tuple2, Tuple3, ... Tuple20) Tuple2<byte[],BigInteger> result = ct2.getInfo().send(); System.out.println("info: " + result.getValue1() + "," + result.getValue2()); web3j.shutdown(); } catch(Exception e) { System.out.println("exception: " + e); } } }
web3j.shutdown(); OKHttpClient.connectionPool().evictAll()
private static OkHttpClient createOkHttpClient() { final OkHttpClient.Builder builder = new OkHttpClient.Builder().connectionSpecs(CONNECTION_SPEC_LIST); return builder.build(); } public MyWeb3Wrapper(String conn_url) { this.httpClient = createOkHttpClient(); this.web3j = Web3j.build(new HttpService(conn_url, this.httpClient)); } public Web3j getweb3j() { return this.web3j; } public void shutdown() { this.web3j.shutdown(); this.httpClient.connectionPool().evictAll(); }
javac -d build/main -cp build/main:lib/epirus-cli-1.4.0-all.jar src/main/java/com/yourcompany/web3/generated/TheContract.java src/main/java/com/yourcompany/web3/apps/deploy1.java src/main/java/com/yourcompany/web3/apps/access1.java
$ java -cp build/main:lib/epirus-cli-1.4.0-all.jar com/yourcompany/web3/apps/deploy1 Connecting to http://127.0.0.1:8545 Loading credentials... Deploying smart contract; be patient... Contract address: 0xe41eaccbe13fc9228bc0f5a33d0114f4fb8ae95c
$ java -cp build/main:lib/epirus-cli-1.4.0-all.jar com/yourcompany/web3/apps/access1 0xe41eaccbe13fc9228bc0f5a33d0114f4fb8ae95c Connecting to http://127.0.0.1:8545 Loading credentials... cpty: CPTY1 TX status: 0x1 TX root: null TX TXhash: 0x974c1127e1a4553d3c88e638e63e4128cc088d4d2abeeb3a229d7400686d185d TX blk hash: 0xd3ef76487d2dc9f751ce949c861d97d87e296cc04f9320574c8474bfb9406238 TX blk num: 34 TX gas used: 28842 TX from: 0xad2a9bde6c3fe0149c29db01ed40966b1b06f3d0 TX to: 0xe41eaccbe13fc9228bc0f5a33d0114f4fb8ae95c info: [B@21aa6d6c,1000
-rw-rw-r-- 1 swguy staffr 57030250 Nov 12 12:51 epirus-cli-1.4.0-all.jar $ jar tf epirus-cli-1.4.0-all.jar | wc -l 34998 -rw-rw-r-- 1 swguy staff 108540653 Aug 18 17:56 epirus-cli-1.2.4-all.jar $ jar tvf epirus-cli-1.2.4-all.jar | wc -l 65358
Like this? Dislike this? Let me know