/*
* Copyright (c) [2016] [ <ether.camp> ]
* This file is part of the ethereumJ library.
*
* The ethereumJ library is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* The ethereumJ library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with the ethereumJ library. If not, see <http://www.gnu.org/licenses/>.
*/
package org.ethereum.samples;
import org.ethereum.core.CallTransaction;
import org.ethereum.facade.EthereumFactory;
import org.ethereum.util.ByteUtil;
import org.ethereum.util.Utils;
import org.ethereum.vm.program.ProgramResult;
import org.spongycastle.util.encoders.Hex;
import org.springframework.context.annotation.Bean;
import java.math.BigInteger;
import java.util.Date;
/**
* This sample demonstrates how constant calls works (that is transactions which are
* not broadcasted to network, executed locally, don't change the blockchain state and can
* report function return values).
* Constant calls can actually invoke contract functions which are not formally 'const'
* i.e. which change the contract storage state, but after such calls the contract
* storage will remain unmodified.
*
* As a side effect this sample shows how Java wrappers for Ethereum contracts can be
* created and then manipulated as regular Java objects
*
* Created by Anton Nashatyrev on 05.02.2016.
*/
public class PriceFeedSample extends BasicSample {
/**
* Base class for a Ethereum Contract wrapper
* It can be used by two ways:
* 1. for each function specify its name and input/output formal parameters
* 2. Pass the contract JSON ABI to the constructor and then refer the function by name only
*/
abstract class EthereumContract {
private final static String zeroAddr = "0000000000000000000000000000000000000000";
private final String contractAddr;
private CallTransaction.Contract contractFromABI = null;
/**
* @param contractAddr address of the target contract as a hex String
*/
protected EthereumContract(String contractAddr) {
this.contractAddr = contractAddr;
}
/**
* Use this variant if you have the contract ABI then you call the functions
* by their names only
*/
public EthereumContract(String contractAddr, String contractABI) {
this.contractAddr = contractAddr;
this.contractFromABI = new CallTransaction.Contract(contractABI);
}
/**
* The main method of this demo which illustrates how to call a constant function.
* To identify the Solidity contract function (calculate its signature) we need :
* - function name
* - a list of function formal params how they are declared in declaration
* Output parameter types are required only for decoding the return values.
*
* Input arguments Java -> Solidity mapping is the following:
* Number, BigInteger, String (hex) -> any integer type
* byte[], String (hex) -> bytesN, byte[]
* String -> string
* Java array of the above types -> Solidity dynamic array of the corresponding type
*
* Output arguments Solidity -> Java mapping:
* any integer type -> BigInteger
* string -> String
* bytesN, byte[] -> byte[]
* Solidity dynamic array -> Java array
*/
protected Object[] callFunction(String name, String[] inParamTypes, String[] outParamTypes, Object ... args) {
CallTransaction.Function function = CallTransaction.Function.fromSignature(name, inParamTypes, outParamTypes);
ProgramResult result = ethereum.callConstantFunction(contractAddr, function, args);
return function.decodeResult(result.getHReturn());
}
/**
* Use this method if the contract ABI was passed
*/
protected Object[] callFunction(String functionName, Object ... args) {
if (contractFromABI == null) {
throw new RuntimeException("The contract JSON ABI should be passed to constructor to use this method");
}
CallTransaction.Function function = contractFromABI.getByName(functionName);
ProgramResult result = ethereum.callConstantFunction(contractAddr, function, args);
return function.decodeResult(result.getHReturn());
}
/**
* Checks if the contract exist in the repository
*/
public boolean isExist() {
return !contractAddr.equals(zeroAddr) && ethereum.getRepository().isExist(Hex.decode(contractAddr));
}
}
/**
* NameReg contract which manages a registry of contracts which can be accessed by name
*
* Here we resolve contract functions by specifying their name and input/output types
*
* Contract sources, live state and many more here:
* https://live.ether.camp/account/985509582b2c38010bfaa3c8d2be60022d3d00da
*/
class NameRegContract extends EthereumContract {
public NameRegContract() {
super("985509582b2c38010bfaa3c8d2be60022d3d00da");
}
public byte[] addressOf(String name) {
BigInteger bi = (BigInteger) callFunction("addressOf", new String[] {"bytes32"}, new String[] {"address"}, name)[0];
return ByteUtil.bigIntegerToBytes(bi, 20);
}
public String nameOf(byte[] addr) {
return (String) callFunction("nameOf", new String[]{"address"}, new String[]{"bytes32"}, addr)[0];
}
}
/**
* PriceFeed contract where prices for several securities are stored and updated periodically
*
* This contract is created using its JSON ABI representation
*
* Contract sources, live state and many more here:
* https://live.ether.camp/account/1194e966965418c7d73a42cceeb254d875860356
*/
class PriceFeedContract extends EthereumContract {
private static final String contractABI =
"[{" +
" 'constant': true," +
" 'inputs': [{" +
" 'name': 'symbol'," +
" 'type': 'bytes32'" +
" }]," +
" 'name': 'getPrice'," +
" 'outputs': [{" +
" 'name': 'currPrice'," +
" 'type': 'uint256'" +
" }]," +
" 'type': 'function'" +
"}, {" +
" 'constant': true," +
" 'inputs': [{" +
" 'name': 'symbol'," +
" 'type': 'bytes32'" +
" }]," +
" 'name': 'getTimestamp'," +
" 'outputs': [{" +
" 'name': 'timestamp'," +
" 'type': 'uint256'" +
" }]," +
" 'type': 'function'" +
"}, {" +
" 'constant': true," +
" 'inputs': []," +
" 'name': 'updateTime'," +
" 'outputs': [{" +
" 'name': ''," +
" 'type': 'uint256'" +
" }]," +
" 'type': 'function'" +
"}, {" +
" 'inputs': []," +
" 'type': 'constructor'" +
"}]";
public PriceFeedContract(String contractAddr) {
super(contractAddr, contractABI.replace("'", "\"")); //JSON parser doesn't like single quotes :(
}
public Date updateTime() {
BigInteger ret = (BigInteger) callFunction("updateTime")[0];
// All times in Ethereum are Unix times
return new Date(Utils.fromUnixTime(ret.longValue()));
}
public double getPrice(String ticker) {
BigInteger ret = (BigInteger) callFunction("getPrice", ticker)[0];
// since Ethereum has no decimal numbers we are storing prices with
// virtual fixed point
return ret.longValue() / 1_000_000d;
}
public Date getTimestamp(String ticker) {
BigInteger ret = (BigInteger) callFunction("getTimestamp", ticker)[0];
return new Date(Utils.fromUnixTime(ret.longValue()));
}
}
@Override
public void onSyncDone() {
try {
// after all blocks are synced perform the work
// worker();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
protected void waitForDiscovery() throws Exception {
super.waitForDiscovery();
worker();
}
/**
* The method retrieves the information from the PriceFeed contract once in a minute and prints
* the result in log.
*/
private void worker() throws Exception{
NameRegContract nameRegContract = new NameRegContract();
if (!nameRegContract.isExist()) {
throw new RuntimeException("Namereg contract not exist on the blockchain");
}
String priceFeedAddress = Hex.toHexString(nameRegContract.addressOf("ether-camp/price-feed"));
logger.info("Got PriceFeed address from name registry: " + priceFeedAddress);
PriceFeedContract priceFeedContract = new PriceFeedContract(priceFeedAddress);
logger.info("Polling cryptocurrency exchange rates once a minute (prices are normally updated each 10 mins)...");
String[] tickers = {"BTC_ETH", "USDT_BTC", "USDT_ETH"};
while(true) {
if (priceFeedContract.isExist()) {
String s = priceFeedContract.updateTime() + ": ";
for (String ticker : tickers) {
s += ticker + " " + priceFeedContract.getPrice(ticker) + " (" + priceFeedContract.getTimestamp(ticker) + "), ";
}
logger.info(s);
} else {
logger.info("PriceFeed contract not exist. Likely it was not yet created until current block");
}
Thread.sleep(60 * 1000);
}
}
private static class Config {
@Bean
public PriceFeedSample priceFeedSample() {
return new PriceFeedSample();
}
}
public static void main(String[] args) throws Exception {
sLogger.info("Starting EthereumJ!");
// Based on Config class the sample would be created by Spring
// and its springInit() method would be called as an entry point
EthereumFactory.createEthereum(Config.class);
}
}