All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.fisco.bcos.web3j.solidity.Abi Maven / Gradle / Ivy

/*
 * Copyright (c) [2016] [  ]
 * 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 .
 */
package org.fisco.bcos.web3j.solidity;

import static com.fasterxml.jackson.annotation.JsonInclude.Include;
import static java.lang.String.format;
import static org.apache.commons.collections4.ListUtils.select;
import static org.apache.commons.lang3.ArrayUtils.subarray;
import static org.apache.commons.lang3.StringUtils.join;
import static org.apache.commons.lang3.StringUtils.stripEnd;
import static org.fisco.bcos.web3j.crypto.Hash.sha3;
import static org.fisco.bcos.web3j.solidity.SolidityType.IntType.decodeInt;
import static org.fisco.bcos.web3j.solidity.SolidityType.IntType.encodeInt;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.Predicate;
import org.fisco.bcos.web3j.utils.ByteUtil;

public class Abi extends ArrayList {
  private static final ObjectMapper DEFAULT_MAPPER =
      new ObjectMapper()
          .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
          .enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL);

  public static Abi fromJson(String json) {
    try {
      return DEFAULT_MAPPER.readValue(json, Abi.class);
    } catch (IOException e) {
      throw new RuntimeException(e);
    }
  }

  public String toJson() {
    try {
      return new ObjectMapper().writeValueAsString(this);
    } catch (JsonProcessingException e) {
      throw new RuntimeException(e);
    }
  }

  private  T find(
      Class resultClass, final Entry.Type type, final Predicate searchPredicate) {
    return (T)
        CollectionUtils.find(
            this, entry -> entry.type == type && searchPredicate.evaluate((T) entry));
  }

  public Function findFunction(Predicate searchPredicate) {
    return find(Function.class, Entry.Type.function, searchPredicate);
  }

  public Event findEvent(Predicate searchPredicate) {
    return find(Event.class, Entry.Type.event, searchPredicate);
  }

  public Constructor findConstructor() {
    return find(Constructor.class, Entry.Type.constructor, object -> true);
  }

  @Override
  public String toString() {
    return toJson();
  }

  @JsonInclude(Include.NON_NULL)
  public abstract static class Entry {

    public enum Type {
      constructor,
      function,
      event,
      fallback
    }

    @JsonInclude(Include.NON_NULL)
    public static class Param {
      public Boolean indexed;
      public String name;
      public SolidityType type;

      public static List decodeList(List params, byte[] encoded) {
        List result = new ArrayList<>(params.size());

        int offset = 0;
        for (Param param : params) {
          Object decoded =
              param.type.isDynamicType()
                  ? param.type.decode(encoded, decodeInt(encoded, offset).intValue())
                  : param.type.decode(encoded, offset);
          result.add(decoded);

          offset += param.type.getFixedSize();
        }

        return result;
      }

      @Override
      public String toString() {
        return format(
            "%s%s%s",
            type.getCanonicalName(), (indexed != null && indexed) ? " indexed " : " ", name);
      }
    }

    public final Boolean anonymous;
    public final Boolean constant;
    public final String name;
    public final List inputs;
    public final List outputs;
    public final Type type;
    public final Boolean payable;

    public Entry(
        Boolean anonymous,
        Boolean constant,
        String name,
        List inputs,
        List outputs,
        Type type,
        Boolean payable) {
      this.anonymous = anonymous;
      this.constant = constant;
      this.name = name;
      this.inputs = inputs;
      this.outputs = outputs;
      this.type = type;
      this.payable = payable;
    }

    public String formatSignature() {
      StringBuilder paramsTypes = new StringBuilder();
      for (Entry.Param param : inputs) {
        paramsTypes.append(param.type.getCanonicalName()).append(",");
      }

      return format("%s(%s)", name, stripEnd(paramsTypes.toString(), ","));
    }

    public byte[] fingerprintSignature() {
      return sha3(formatSignature().getBytes());
    }

    public byte[] encodeSignature() {
      return fingerprintSignature();
    }

    @JsonCreator
    public static Entry create(
        @JsonProperty("anonymous") boolean anonymous,
        @JsonProperty("constant") boolean constant,
        @JsonProperty("name") String name,
        @JsonProperty("inputs") List inputs,
        @JsonProperty("outputs") List outputs,
        @JsonProperty("type") Type type,
        @JsonProperty(value = "payable", required = false, defaultValue = "false")
            Boolean payable) {
      Entry result = null;
      switch (type) {
        case constructor:
          result = new Constructor(inputs, outputs);
          break;
        case function:
        case fallback:
          result = new Function(constant, name, inputs, outputs, payable);
          break;
        case event:
          result = new Event(anonymous, name, inputs, outputs);
          break;
      }

      return result;
    }
  }

  public static class Constructor extends Entry {

    public Constructor(List inputs, List outputs) {
      super(null, null, "", inputs, outputs, Type.constructor, false);
    }

    public List decode(byte[] encoded) {
      return Param.decodeList(inputs, encoded);
    }

    public String formatSignature(String contractName) {
      return format("function %s(%s)", contractName, join(inputs, ", "));
    }
  }

  public static class Function extends Entry {

    private static final int ENCODED_SIGN_LENGTH = 4;

    public Function(
        boolean constant, String name, List inputs, List outputs, Boolean payable) {
      super(null, constant, name, inputs, outputs, Type.function, payable);
    }

    public byte[] encode(Object... args) {
      return ByteUtil.merge(encodeSignature(), encodeArguments(args));
    }

    private byte[] encodeArguments(Object... args) {
      if (args.length > inputs.size())
        throw new RuntimeException("Too many arguments: " + args.length + " > " + inputs.size());

      int staticSize = 0;
      int dynamicCnt = 0;
      // calculating static size and number of dynamic params
      for (int i = 0; i < args.length; i++) {
        SolidityType type = inputs.get(i).type;
        if (type.isDynamicType()) {
          dynamicCnt++;
        }
        staticSize += type.getFixedSize();
      }

      byte[][] bb = new byte[args.length + dynamicCnt][];
      for (int curDynamicPtr = staticSize, curDynamicCnt = 0, i = 0; i < args.length; i++) {
        SolidityType type = inputs.get(i).type;
        if (type.isDynamicType()) {
          byte[] dynBB = type.encode(args[i]);
          bb[i] = encodeInt(curDynamicPtr);
          bb[args.length + curDynamicCnt] = dynBB;
          curDynamicCnt++;
          curDynamicPtr += dynBB.length;
        } else {
          bb[i] = type.encode(args[i]);
        }
      }

      return ByteUtil.merge(bb);
    }

    public List decode(byte[] encoded) {
      return Param.decodeList(inputs, subarray(encoded, ENCODED_SIGN_LENGTH, encoded.length));
    }

    public List decodeResult(byte[] encoded) {
      return Param.decodeList(outputs, encoded);
    }

    @Override
    public byte[] encodeSignature() {
      return extractSignature(super.encodeSignature());
    }

    public static byte[] extractSignature(byte[] data) {
      return subarray(data, 0, ENCODED_SIGN_LENGTH);
    }

    @Override
    public String toString() {
      String returnTail = "";
      if (constant) {
        returnTail += " constant";
      }
      if (!outputs.isEmpty()) {
        List types = new ArrayList<>();
        for (Param output : outputs) {
          types.add(output.type.getCanonicalName());
        }
        returnTail += format(" returns(%s)", join(types, ", "));
      }

      return format("function %s(%s)%s;", name, join(inputs, ", "), returnTail);
    }
  }

  public static class Event extends Entry {

    public Event(boolean anonymous, String name, List inputs, List outputs) {
      super(anonymous, null, name, inputs, outputs, Type.event, false);
    }

    public List decode(byte[] data, byte[][] topics) {
      List result = new ArrayList<>(inputs.size());

      byte[][] argTopics = anonymous ? topics : subarray(topics, 1, topics.length);
      List indexedParams = filteredInputs(true);
      List indexed = new ArrayList<>();
      for (int i = 0; i < indexedParams.size(); i++) {
        Object decodedTopic;
        if (indexedParams.get(i).type.isDynamicType()) {
          // If arrays (including string and bytes) are used as indexed arguments,
          // the Keccak-256 hash of it is stored as topic instead.
          decodedTopic = SolidityType.Bytes32Type.decodeBytes32(argTopics[i], 0);
        } else {
          decodedTopic = indexedParams.get(i).type.decode(argTopics[i]);
        }
        indexed.add(decodedTopic);
      }
      List notIndexed = Param.decodeList(filteredInputs(false), data);

      for (Param input : inputs) {
        result.add(input.indexed ? indexed.remove(0) : notIndexed.remove(0));
      }

      return result;
    }

    private List filteredInputs(final boolean indexed) {
      return select(inputs, param -> param.indexed == indexed);
    }

    @Override
    public String toString() {
      return format("event %s(%s);", name, join(inputs, ", "));
    }
  }
}